update code

This commit is contained in:
Evgeny @ SimpleX Chat committed 2026-10-21 13:48:53 +00:00
1 parent 2a86c24a57
commit f39002a146
21 files changed
+752 -335

No files matched your search

+1 -1
View File
@@ -123,7 +123,7 @@ chatCommandsDocsData =
("APIJoinGroup", ["enableNtfs"], "Join group.", ["CRUserAcceptedGroupSent", "CRChatCmdError"], [], Just UNInteractive, "/_join #" <> Param "groupId"),
("APIAcceptMember", [], "Accept group member. Requires Admin role.", ["CRMemberAccepted", "CRChatCmdError"], [TD "CEGroupMemberNotActive" "Member is not connected yet"], Just UNBackground, "/_accept member #" <> Param "groupId" <> " " <> Param "groupMemberId" <> " " <> Param "memberRole"),
("APIMembersRole", [], "Set members role. Requires Admin role.", ["CRMembersRoleUser", "CRChatCmdError"], [], Just UNBackground, "/_member role #" <> Param "groupId" <> " " <> Join ',' "groupMemberIds" <> " " <> Param "memberRole"),
("APIBlockMembersForAll", [], "Block members. Requires Moderator role.", ["CRMembersBlockedForAllUser", "CRChatCmdError"], [], Just UNBackground, "/_block #" <> Param "groupId" <> " " <> Join ',' "groupMemberIds" <> OnOffParam "blocked" "blocked" Nothing),
("APIBlockMembersForAll", [], "Block members. Requires Moderator role.", ["CRMembersBlockedForAllUser", "CRChatCmdError"], [], Just UNBackground, "/_block #" <> Param "groupId" <> " " <> Join ',' "groupMemberIds" <> OnOffParam "blocked" "blocked" Nothing <> Optional "" (" ttl=" <> Param "$0") "ttl"),
("APIRemoveMembers", [], "Remove members. Requires Admin role.", ["CRUserDeletedMembers", "CRChatCmdError"], ["CEGroupMemberNotFound"], Just UNBackground, "/_remove #" <> Param "groupId" <> " " <> Join ',' "groupMemberIds" <> OnOffParam "messages" "withMessages" (Just False) <> Optional "" (" notice=" <> Json "$0") "notice"),
("APILeaveGroup", [], "Leave group.", ["CRLeftMemberUser", "CRChatCmdError"], [], Just UNBackground, "/_leave #" <> Param "groupId"),
("APIListMembers", [], "Get group members.", ["CRGroupMembers", "CRChatCmdError"], [], Nothing, "/_members #" <> Param "groupId"),
+1 -1
View File
@@ -229,7 +229,7 @@ chatTypesDocsData =
(sti @BusinessChatInfo, STRecord, "", [], "", ""),
(sti @BusinessChatType, STEnum, "BC", [], "", ""),
(sti @ChatBotCommand, STUnion, "CBC", [], "", ""),
(sti @ChatDeleteMode, STUnion, "CDM", [], Param "type" <> Choice "self" [("messages", "")] (OnOffParam "notify" "notify" (Just True)), ""),
(sti @ChatDeleteMode, STUnion, "CDM", [], Param "type" <> Choice "self" [("messages", ""), ("archive", "")] (OnOffParam "notify" "notify" (Just True)), ""),
(sti @ChatError, STUnion, "Chat", ["ChatErrorDatabase", "ChatErrorRemoteHost", "ChatErrorRemoteCtrl"], "", ""),
(sti @ChatErrorType, STUnion, "CE", ["CEContactNotFound", "CEServerProtocol", "CECallState", "CEInvalidChatMessage"], "", ""),
(sti @BadgeStatus, STEnum, "BS", [], "", ""),
+9 -4
View File
@@ -252,7 +252,7 @@ Currently members can have one of four roles - `owner`, `admin`, `member` and `o
`x.grp.mem.role` message is sent to update group member role - it is sent to all members by the member who updated the role of the member referenced in this message. This message MUST only be sent by members with `admin` or `owner` role. Receiving clients MUST ignore this message if it is received from member with role less than `admin`.
`x.grp.mem.restrict` message is sent to group members to communicate group member restrictions, such as member being blocked for sending messages.
`x.grp.mem.restrict` message is sent to group members to communicate group member restrictions, such as member being blocked for sending messages. The restrictions MAY include `expiresAt` time set by the sender, when the block ends. A block also ends when the member is unblocked, and it does not apply to a member who leaves and joins again. When a block with `expiresAt` ends, the client of the member who set it sends `x.grp.mem.restrict` with `unrestricted` restriction and `expiresAt` of the ended block. Clients with chat protocol version 23 or later ignore this message, as they end the block at `expiresAt`.
`x.grp.mem.con` message is sent by members connecting inside group to inviting member, to notify the inviting member they have completed the connection and no longer require forwarding messages between them.
@@ -260,7 +260,7 @@ Currently members can have one of four roles - `owner`, `admin`, `member` and `o
`x.grp.leave` message is sent to all members by the member leaving the group. If the only group `owner` leaves the group, it will not be possible to delete it with `x.grp.del` message - but all members can still leave the group with `x.grp.leave` message and then delete a local copy of the group.
`x.grp.del` message is sent to all members by the member who deletes the group. Clients who received this message SHOULD keep a local copy of the deleted group, until it is deleted by the user. This message MUST only be sent by members with `owner` role. Receiving clients MUST ignore this message if it is received from member other than with `owner` role. Receiving clients apply optional `notice` property to the business address of a deleted business chat (see [Link notices](#link-notices)).
`x.grp.del` message is sent to all members by the member who deletes the group. Clients who received this message SHOULD keep a local copy of the deleted group, until it is deleted by the user. This message MUST only be sent by members with `owner` role. Receiving clients MUST ignore this message if it is received from member other than with `owner` role.
`x.grp.info` message is sent to all members by the member who updated group profile. Only group owners can update group profiles. Clients MAY implement some conflict resolution strategy - it is currently not implemented by SimpleX Chat client. This message MUST only be sent by members with `owner` role. Receiving clients MUST ignore this message if it is received from member other than with `owner` role.
@@ -270,9 +270,14 @@ Currently members can have one of four roles - `owner`, `admin`, `member` and `o
### Link notices
`x.grp.mem.del`, `x.grp.del` and `x.direct.del` messages MAY include `notice` property with optional `ttl` (seconds) and optional `reason` (`spam`, `content`, `community`, `profile` or `other`). The receiving client stores the notice for the link it joined or connected through, as recorded in its own group or contact record, and does not connect via this link until the notice expires. The notice expires `ttl` seconds after the server timestamp of the message; a notice without `ttl` does not expire. A new notice for the same link replaces the previous one. Clients MUST ignore a malformed notice and process the message.
`x.grp.mem.del` and `x.direct.del` messages MAY include `notice` property with optional `ttl` (seconds). The receiving client stores the notice for the link it joined or connected through, as recorded in its own group or contact record, and does not connect via this link until the notice expires. The notice expires `ttl` seconds after the server timestamp of the message; a notice without `ttl` does not expire. A new notice for the same link replaces the previous one. Clients MUST ignore a malformed notice and process the message.
When `x.direct.del` message has `"silent": true` property, the receiving client stores the notice and does not delete the contact. Clients send it when the contact is deleted without notification, only to contacts with chat protocol version 23 or later.
`x.grp.mem.del` and `x.direct.del` messages MAY include `"silent": true` property:
- the client of the deleted contact does not delete the contact and shows nothing.
- the client of the removed member shows nothing, keeps the group and stops sending messages to its members. A client that forwards group messages (an admin or owner in a group without relays, a relay in a channel) processes its own removal as without this property.
- clients of other members do not show the removal.
Clients use `silent` and `notice` of a forwarded `x.grp.mem.del` message only when it is forwarded by a relay with a verified signature. Clients send `notice` only with `silent`, and only to members and contacts with chat protocol version 23 or later.
### Channels: relay-mediated groups
+27 -1
View File
@@ -311,6 +311,24 @@
"memberRestrictions": {
"properties": {
"restriction": {"ref": "memberRestrictionStatus"}
},
"optionalProperties": {
"expiresAt": {
"type": "string",
"metadata": {
"format": "ISO8601 UTC time when the block ends"
}
}
}
},
"linkNotice": {
"optionalProperties": {
"ttl": {
"type": "integer",
"metadata": {
"format": "seconds from the server timestamp of the message"
}
}
}
},
"memberRestrictionStatus": {
@@ -551,7 +569,11 @@
"properties": {
"msgId": {"ref": "base64url"},
"params": {
"properties": {}
"properties": {},
"optionalProperties": {
"silent": {"type": "boolean"},
"notice": {"ref": "linkNotice"}
}
}
}
},
@@ -692,6 +714,10 @@
"params": {
"properties": {
"memberId": {"ref": "base64url"}
},
"optionalProperties": {
"silent": {"type": "boolean"},
"notice": {"ref": "linkNotice"}
}
}
}
+7 -6
View File
@@ -436,7 +436,7 @@ data ChatCommand
| APIChatRead {chatRef :: ChatRef}
| APIChatItemsRead {chatRef :: ChatRef, chatItemIds :: NonEmpty ChatItemId}
| APIChatUnread {chatRef :: ChatRef, unreadChat :: Bool}
| APIDeleteChat {chatRef :: ChatRef, chatDeleteMode :: ChatDeleteMode, notice :: Maybe LinkNotice} -- currently delete mode settings are only applied to direct chats
| APIDeleteChat {chatRef :: ChatRef, chatDeleteMode :: ChatDeleteMode, notice :: Maybe LinkNotice}
| APIClearChat {chatRef :: ChatRef}
| APIAcceptContact {incognito :: IncognitoEnabled, contactReqId :: Int64}
| APIRejectContact {contactReqId :: Int64, notify :: Bool}
@@ -474,7 +474,7 @@ data ChatCommand
| APIAcceptMember {groupId :: GroupId, groupMemberId :: GroupMemberId, memberRole :: GroupMemberRole}
| APIDeleteMemberSupportChat GroupId GroupMemberId
| APIMembersRole {groupId :: GroupId, groupMemberIds :: NonEmpty GroupMemberId, memberRole :: GroupMemberRole}
| APIBlockMembersForAll {groupId :: GroupId, groupMemberIds :: NonEmpty GroupMemberId, blocked :: Bool}
| APIBlockMembersForAll {groupId :: GroupId, groupMemberIds :: NonEmpty GroupMemberId, blocked :: Bool, ttl :: Maybe Int}
| APIRemoveMembers {groupId :: GroupId, groupMemberIds :: NonEmpty GroupMemberId, withMessages :: Bool, notice :: Maybe LinkNotice}
| APILeaveGroup {groupId :: GroupId}
| APIListMembers {groupId :: GroupId}
@@ -623,7 +623,7 @@ data ChatCommand
| JoinGroup {groupName :: GroupName, enableNtfs :: MsgFilter}
| AcceptMember GroupName ContactName GroupMemberRole
| MemberRole GroupName ContactName GroupMemberRole
| BlockForAll GroupName ContactName Bool
| BlockForAll GroupName ContactName Bool (Maybe Int)
| RemoveMembers {groupName :: GroupName, members :: NonEmpty ContactName, withMessages :: Bool, notice :: Maybe LinkNotice}
| LeaveGroup GroupName
| AllowRelayGroup GroupName
@@ -1162,6 +1162,7 @@ data ChatDeleteMode
= CDMFull {notify :: Bool} -- delete both contact and conversation
| CDMEntity {notify :: Bool} -- delete contact (connection), keep conversation
| CDMMessages -- delete conversation, keep contact - can be re-opened from Contacts view
| CDMArchive
deriving (Show)
data ConnectionPlan
@@ -1185,7 +1186,7 @@ data ContactAddressPlan
| CAPConnectingProhibit {contact :: Contact}
| CAPKnown {contact :: Contact}
| CAPContactViaAddress {contact :: Contact}
| CAPLinkNotice {expiresAt :: Maybe UTCTime, reason :: Maybe ReportReason}
| CAPLinkNotice {expiresAt :: Maybe UTCTime, contactSLinkData_ :: Maybe ContactShortLinkData}
deriving (Show)
data GroupLinkPlan
@@ -1196,7 +1197,7 @@ data GroupLinkPlan
| GLPKnown {groupInfo :: GroupInfo, groupUpdated :: Bool, ownerVerification :: Maybe OwnerVerification, linkOwners :: ListDef GroupLinkOwner}
| GLPNoRelays {groupSLinkData_ :: Maybe GroupShortLinkData}
| GLPUpdateRequired {groupSLinkData_ :: Maybe GroupShortLinkData}
| GLPLinkNotice {expiresAt :: Maybe UTCTime, reason :: Maybe ReportReason}
| GLPLinkNotice {expiresAt :: Maybe UTCTime}
deriving (Show)
data GroupLinkOwner = GroupLinkOwner
@@ -1534,7 +1535,7 @@ data ChatErrorType
| CEUnsupportedConnReq
| CEInvalidChatMessage {connection :: Connection, msgMeta :: Maybe MsgMetaJSON, messageData :: Text, message :: String}
| CEConnReqMessageProhibited
| CELinkNotice {expiresAt :: Maybe UTCTime, reason :: Maybe ReportReason}
| CELinkNotice {expiresAt :: Maybe UTCTime}
| CEContactNotFound {contactName :: ContactName, suspectedMember :: Maybe (GroupInfo, GroupMember)}
| CEContactNotReady {contact :: Contact}
| CEContactNotActive {contact :: Contact}
+152 -87
View File
@@ -1355,6 +1355,7 @@ processChatCommand cxt nm = \case
APIDeleteChat cRef@(ChatRef cType chatId scope) cdm notice_ -> withUser $ \user@User {userId} -> case cType of
CTDirect -> do
ct <- withFastStore $ \db -> getContact db cxt user chatId
when (isJust notice_ && not (noticeSupported ct)) $ throwCmdError "contact does not support notice"
filesInfo <- withFastStore' $ \db -> getContactFileInfo db user ct
withContactLock "deleteChat direct" chatId $
case cdm of
@@ -1381,54 +1382,79 @@ processChatCommand cxt nm = \case
void $ processChatCommand cxt nm $ APIClearChat cRef
withFastStore' $ \db -> setContactChatDeleted db user ct True
pure $ CRContactDeleted user ct {chatDeleted = True}
CDMArchive -> do
when (isJust notice_) $ throwCmdError "notice requires deleting the contact"
withFastStore' $ \db -> setContactChatDeleted db user ct True
pure $ CRContactDeleted user ct {chatDeleted = True}
where
noticeSupported ct = contactReady ct && contactActive ct && any (\Connection {peerChatVRange} -> maxVersion peerChatVRange >= linkNoticeVersion) (contactConn ct)
sendDelDeleteConns ct notify = do
let delMsg_
| not (contactReady ct && contactActive ct) = Nothing
| notify = Just $ XDirectDel False notice_
| isJust notice_ && any (\Connection {peerChatVRange} -> maxVersion peerChatVRange >= linkNoticeVersion) (contactConn ct) = Just $ XDirectDel True notice_
| otherwise = Nothing
forM_ delMsg_ $ \msg -> void (sendDirectContactMessage user ct msg) `catchAllErrors` const (pure ())
let doSendDel = contactReady ct && contactActive ct && (notify || isJust notice_)
when doSendDel $ void (sendDirectContactMessage user ct (XDirectDel (isJust notice_) notice_)) `catchAllErrors` const (pure ())
contactConnIds <- map aConnId <$> withFastStore' (\db -> getContactConnections db cxt userId ct)
deleteAgentConnectionsAsync' contactConnIds (isJust delMsg_)
deleteAgentConnectionsAsync' contactConnIds doSendDel
CTContactConnection -> withConnectionLock "deleteChat contactConnection" chatId $ do
when (isJust notice_) $ throwCmdError "notice is not supported for pending connections"
case cdm of
CDMArchive -> throwCmdError "archive is not supported for pending connections"
_ -> pure ()
conn@PendingContactConnection {pccAgentConnId = AgentConnId acId} <- withFastStore $ \db -> getPendingContactConnection db userId chatId
deleteAgentConnectionAsync acId
withFastStore' $ \db -> deletePendingContactConnection db userId chatId
pure $ CRContactConnectionDeleted user conn
CTGroup | isNothing scope -> do
g@(GIK gInfo@GroupInfo {membership} _) <- withFastStore $ \db -> getGroupInfoKeys db cxt user chatId
g@(GIK gInfo@GroupInfo {membership, businessChat} _) <- withFastStore $ \db -> getGroupInfoKeys db cxt user chatId
let isOwner = memberRole' membership == GROwner
canDelete = isOwner || not (memberCurrent membership)
doSendDel = memberActive membership && isOwner
sendDelDeleteConns = do
(members, recipients) <- getRecipients gInfo
msgSigned <-
if doSendDel
then (\SndMessage {signedMsg_} -> isJust signedMsg_) <$> sendGroupMessage' user g recipients XGrpDel
else pure False
deleteGroupLinkIfExists user gInfo
deleteMembersConnections' user members doSendDel
updateCIGroupInvitationStatus user gInfo CIGISRejected `catchAllErrors` \_ -> pure ()
withFastStore' $ \db -> cleanupHostGroupLinkConn db user gInfo
pure msgSigned
unless canDelete $ throwChatError $ CEGroupUserRole gInfo GROwner
filesInfo <- withFastStore' $ \db -> getGroupFileInfo db user gInfo
withGroupLock "deleteChat group" chatId $ do
deleteCIFiles user filesInfo
-- the roster blob file has no chat item, so it is missed by getGroupFileInfo above
cleanupGroupRosterFile user gInfo
(members, recipients) <- getRecipients gInfo
let doSendDel = memberActive membership && isOwner
msgSigned <-
if doSendDel
then (\SndMessage {signedMsg_} -> isJust signedMsg_) <$> sendGroupMessage' user g recipients (XGrpDel notice_)
else pure False
deleteGroupLinkIfExists user gInfo
deleteMembersConnections' user members doSendDel
updateCIGroupInvitationStatus user gInfo CIGISRejected `catchAllErrors` \_ -> pure ()
withFastStore' $ \db -> deleteGroupChatItems db user gInfo
withFastStore' $ \db -> cleanupHostGroupLinkConn db user gInfo
withFastStore' $ \db -> deleteGroupMembers db user gInfo
withFastStore' $ \db -> deleteGroup db user gInfo
pure $ CRGroupDeletedUser user gInfo msgSigned (not doSendDel)
where
getRecipients gInfo
| useRelays' gInfo = do
relays <- withFastStore' $ \db -> getGroupRelayMembers db cxt user gInfo
pure (relays, relays)
| otherwise = do
ms <- withFastStore' $ \db -> getGroupMembers db cxt user gInfo
pure (ms, filter memberCurrentOrPending ms)
case cdm of
CDMFull _ -> do
removeCustomer gInfo businessChat
withGroupLock "deleteChat group" chatId $ do
deleteCIFiles user filesInfo
-- the roster blob file has no chat item, so it is missed by getGroupFileInfo above
cleanupGroupRosterFile user gInfo
msgSigned <- sendDelDeleteConns
withFastStore' $ \db -> deleteGroupChatItems db user gInfo
withFastStore' $ \db -> deleteGroupMembers db user gInfo
withFastStore' $ \db -> deleteGroup db user gInfo
pure $ CRGroupDeletedUser user gInfo msgSigned (not doSendDel)
CDMEntity _
| doSendDel -> do
removeCustomer gInfo businessChat
withGroupLock "deleteChat group" chatId $ do
cancelFilesInProgress user filesInfo
msgSigned <- sendDelDeleteConns
withFastStore' $ \db -> updateGroupMemberStatus db userId membership GSMemGroupDeleted
pure $ CRGroupDeletedUser user gInfo {membership = membership {memberStatus = GSMemGroupDeleted}} msgSigned False
| otherwise -> throwCmdError "keeping the conversation requires an active owner"
_ -> throwCmdError "only full deletion or keeping the conversation is supported for groups"
where
getRecipients gInfo
| useRelays' gInfo = do
relays <- withFastStore' $ \db -> getGroupRelayMembers db cxt user gInfo
pure (relays, relays)
| otherwise = do
ms <- withFastStore' $ \db -> getGroupMembers db cxt user gInfo
pure (ms, filter memberCurrentOrPending ms)
removeCustomer gInfo businessChat = forM_ notice_ $ \notice -> case businessChat of
Just BusinessChatInfo {chatType = BCCustomer, customerId} -> do
GroupMember {groupMemberId} <- withFastStore $ \db -> getGroupMemberByMemberId db cxt user gInfo customerId
void $ processChatCommand cxt nm $ APIRemoveMembers chatId [groupMemberId] False (Just notice)
_ -> throwCmdError "notice is supported only for business chats"
_ -> throwCmdError "not supported"
APIClearChat (ChatRef cType chatId scope) -> withUser $ \user@User {userId} -> case cType of
CTDirect -> do
@@ -2293,7 +2319,6 @@ processChatCommand cxt nm = \case
toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct') ci]
pure $ CRStartedConnectionToContact user ct' customUserProfile
Just PreparedContact {connLinkToConnect = ACCL SCMContact ccLink, welcomeSharedMsgId, requestSharedMsgId} -> do
checkLinkNotice $ connFullLink ccLink
msg_ <- forM msgContent_ $ \mc -> case requestSharedMsgId of
Just smId -> pure (smId, mc)
Nothing -> do
@@ -2385,7 +2410,6 @@ processChatCommand cxt nm = \case
newConnIds <- getAgentConnShortLinkAsync user CFGetRelayDataJoin Nothing relayLink
withStore' $ \db -> createRelayMemberConnectionAsync db user gInfo' relayMember relayLink newConnIds subMode
GroupInfo {preparedGroup = Just PreparedGroup {connLinkToConnect, welcomeSharedMsgId, requestSharedMsgId}} -> do
checkLinkNotice $ connFullLink connLinkToConnect
hostMember <- withFastStore $ \db -> getHostMember db cxt user groupId
msg_ <- forM msgContent_ $ \mc -> case requestSharedMsgId of
Just smId -> pure (smId, mc)
@@ -2415,11 +2439,10 @@ processChatCommand cxt nm = \case
(conn, incognitoProfile) <- connectViaInvitation user incognito ccLink Nothing
let pcc = mkPendingContactConnection conn $ Just ccLink
pure $ CRSentConfirmation user pcc incognitoProfile
ACCL SCMContact ccLink@(CCLink cReq sLnk) -> do
ACCL SCMContact ccLink@(CCLink _ sLnk) -> do
case sLnk of
Just (CSLContact _ CCTChannel _ _) -> throwChatError $ CECommandError "channel links must be connected via APIConnectPreparedGroup"
_ -> pure ()
checkLinkNotice cReq
connectViaContact user Nothing incognito ccLink Nothing Nothing >>= \case
CVRConnectedContact ct -> pure $ CRContactAlreadyExists user ct
CVRSentInvitation conn incognitoProfile -> pure $ CRSentInvitation user (mkPendingContactConnection conn Nothing) incognitoProfile
@@ -2933,6 +2956,7 @@ processChatCommand cxt nm = \case
(g@(GIK gInfo _), m) <- withFastStore $ \db -> (,) <$> getGroupInfoKeys db cxt user groupId <*> getGroupMemberById db cxt user gmId
-- same rule as role change (moderators grant up to member); pending member's role is a stand-in, so treat it as member
assertUserGroupRole gInfo $ roleRequiredToChange GRMember role
when (blockedByAdmin m) $ throwCmdError "can't accept blocked member, unblock first"
case memberStatus m of
GSMemPendingApproval | memberCategory m == GCInviteeMember -> do -- only host can approve
let GroupInfo {groupProfile = GroupProfile {memberAdmission}} = gInfo
@@ -3090,7 +3114,7 @@ processChatCommand cxt nm = \case
updMember db m = do
updateGroupMemberRole db user m newRole
pure (m :: GroupMember) {memberRole = newRole}
APIBlockMembersForAll groupId memberIds blockFlag -> withUser $ \user ->
APIBlockMembersForAll groupId memberIds blockFlag ttl -> withUser $ \user ->
withGroupLock "blockForAll" groupId $ do
-- TODO [relays] possible optimization is to read only required members + relays
(Group gInfo members, gks) <- withFastStore $ \db -> getGroupKeys_ db cxt user groupId
@@ -3099,9 +3123,14 @@ processChatCommand cxt nm = \case
let (blockMems, remainingMems, maxRole, anyAdmin, anyPending) = selectMembers members
when (length blockMems /= length memberIds) $ throwChatError CEGroupMemberNotFound
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"
when (length memberIds > 1 && anyPending) $ throwCmdError "can't block/unblock multiple members when members pending approval selected"
assertUserGroupRole gInfo $ max GRModerator maxRole
blockMembers user (GIK gInfo gks) blockMems remainingMems
now <- liftIO getCurrentTime
let mrs
| blockFlag = MemberRestrictions {restriction = MRSBlocked, expiresAt = capExpiry $ (`addUTCTime` now) . fromIntegral <$> ttl}
| otherwise = MemberRestrictions {restriction = MRSUnrestricted, expiresAt = Nothing}
(blocked, msgSigned) <- blockMembers user (GIK gInfo gks) mrs blockMems remainingMems
pure CRMembersBlockedForAllUser {user, groupInfo = gInfo, members = blocked, blocked = blockFlag, msgSigned}
where
selfSelected GroupInfo {membership} = elem (groupMemberId' membership) memberIds
selectMembers :: [GroupMember] -> ([GroupMember], [GroupMember], GroupMemberRole, Bool, Bool)
@@ -3114,31 +3143,6 @@ processChatCommand cxt nm = \case
anyPending' = anyPending || memberPending m
in (m : block, remaining, maxRole', anyAdmin', anyPending')
| otherwise = (block, m : remaining, maxRole, anyAdmin, anyPending)
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 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
when (length cis_ /= length blockMems) $ logError "blockMembers: blockMems and cis_ length mismatch"
let acis = map (AChatItem SCTGroup SMDSnd (GroupChat gInfo Nothing)) $ rights cis_
unless (null acis) $ toView $ CEvtNewChatItems user acis
(errs, blocked) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (updateGroupMemberBlocked db user gInfo mrs) blockMems)
unless (null errs) $ toView $ CEvtChatErrors errs
-- TODO not batched - requires agent batch api
forM_ blocked $ \m -> toggleNtf m (not blockFlag)
pure CRMembersBlockedForAllUser {user, groupInfo = gInfo, members = blocked, blocked = blockFlag, msgSigned}
where
sndItemData :: GroupMember -> SndMessage -> NewSndChatItemData c
sndItemData GroupMember {groupMemberId, memberProfile} msg =
let content = CISndGroupEvent $ SGEMemberBlocked groupMemberId (fromLocalProfile memberProfile) blockFlag
ts = ciContentTexts content
in NewSndChatItemData msg content ts M.empty Nothing Nothing Nothing
APIRemoveMembers {groupId, groupMemberIds, withMessages, notice} -> withUser $ \user ->
withGroupLock "removeMembers" groupId $ do
-- TODO [relays] possible optimization is to read only required members + relays
@@ -3151,6 +3155,8 @@ processChatCommand cxt nm = \case
assertUserGroupRole gInfo $ max GRAdmin maxRole
when (useRelays' gInfo && anyPrivilegedRemoved && memberRole' (membership gInfo) /= GROwner) $
throwCmdError "only the group owner can remove members, moderators and admins"
when (isJust notice && any noticeUnsupported (invitedMems <> pendingApprvMems <> pendingRvwMems <> currentMems)) $
throwCmdError "notice is not supported for relays, members on old versions and members not in the group"
(errs1, deleted1) <- deleteInvitedMems user invitedMems
let recipients = filter memberCurrent members
let doBumpRoster = useRelays' gInfo && memberRole' (membership gInfo) == GROwner && anyRosterRemoved
@@ -3195,6 +3201,7 @@ processChatCommand cxt nm = \case
-- removed current member: alters the roster blob iff it currently holds a roster role
_ -> (n', invited, pendingApprv, pendingRvw, m : current, maxRole', anyAdmin', anyPrivRemoved', anyRosterRemoved || isRosterRole memberRole)
| otherwise = acc
noticeUnsupported m = not (memberCurrentOrPending m) || isRelay m || not (m `supportsVersion` linkNoticeVersion)
deleteInvitedMems :: User -> [GroupMember] -> CM ([ChatError], [GroupMember])
deleteInvitedMems user memsToDelete = do
deleteMembersConnections user memsToDelete
@@ -3213,7 +3220,7 @@ processChatCommand cxt nm = \case
Nothing -> pure ([], [], [], False)
Just memsToDelete' -> do
let chatScope = toChatScope <$> chatScopeInfo
events = L.map (\GroupMember {memberId} -> XGrpMemDel memberId withMessages rosterVer notice) memsToDelete'
events = L.map (\GroupMember {memberId} -> XGrpMemDel memberId withMessages rosterVer (isJust notice) notice) memsToDelete'
(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_)
@@ -3312,7 +3319,7 @@ processChatCommand cxt nm = \case
processChatCommand cxt nm $ APIJoinGroup groupId enableNtfs
AcceptMember gName gMemberName memRole -> withMemberName gName gMemberName $ \gId gMemberId -> APIAcceptMember gId gMemberId memRole
MemberRole gName gMemberName memRole -> withMemberName gName gMemberName $ \gId gMemberId -> APIMembersRole gId [gMemberId] memRole
BlockForAll gName gMemberName blocked -> withMemberName gName gMemberName $ \gId gMemberId -> APIBlockMembersForAll gId [gMemberId] blocked
BlockForAll gName gMemberName blocked ttl -> withMemberName gName gMemberName $ \gId gMemberId -> APIBlockMembersForAll gId [gMemberId] blocked ttl
RemoveMembers gName gMemberNames withMessages notice -> withUser $ \user -> do
(gId, gMemberIds) <- withStore $ \db -> do
gId <- getGroupIdByName db user gName
@@ -3953,6 +3960,7 @@ processChatCommand cxt nm = \case
Just (PCEGroup (GIK gInfo _) m) | useRelays' gInfo -> Just (memberId' m)
_ -> Nothing
joinPreparedConn' xContactId_ conn@Connection {connId, customUserProfileId} gInfo_ = do
checkLinkNotice cReq
when (incognito /= isJust customUserProfileId) $ throwCmdError "incognito mode is different from prepared connection"
-- TODO [relays] member: refactor joinContact and up avoiding parallel ifs, xContactId is not used
xContactId <- mkXContactId xContactId_
@@ -3961,6 +3969,7 @@ processChatCommand cxt nm = \case
conn' <- joinContact user conn cReq' incognitoProfile reqHeader_ xContactId welcomeSharedMsgId msg_ gInfo_ relayMemberId_ PQSupportOn
pure $ CVRSentInvitation conn' incognitoProfile
connect' groupLinkId xContactId_ gInfo_ = do
checkLinkNotice cReq
let inGroup = isJust groupLinkId
pqSup = if inGroup then PQSupportOff else PQSupportOn
(connId, chatV, reqHeader) <- prepareContact user cReq pqSup
@@ -4022,11 +4031,13 @@ processChatCommand cxt nm = \case
syncSubscriberRelays :: User -> GroupInfo -> [ShortLinkContact] -> CM ()
syncSubscriberRelays user gInfo currentRelayLinks = void . tryAllErrors $ do
localRelayMembers <- withFastStore' $ \db -> getGroupRelayMembers db cxt user gInfo
ts <- liftIO getCurrentTime
hasNotice <- withFastStore' $ \db -> maybe (pure False) (fmap isJust . getLinkNotice db ts . contactCReqHashes) (viaGroupLinkUri gInfo)
let activeRelayMembers = filter memberCurrent localRelayMembers
memberRelayLink GroupMember {relayLink = rl} = rl
localRelayLinks = mapMaybe memberRelayLink activeRelayMembers
newRelayLinks = filter (`notElem` localRelayLinks) currentRelayLinks
forM_ newRelayLinks $ \rlnk -> void . tryAllErrors $
unless hasNotice $ forM_ newRelayLinks $ \rlnk -> void . tryAllErrors $
connectToRelayAsync user gInfo rlnk
forM_ localRelayMembers $ \m ->
case memberRelayLink m of
@@ -4760,22 +4771,22 @@ processChatCommand cxt nm = \case
ts <- liftIO getCurrentTime
withFastStore' (\db -> (,) <$> getUserContactLinkByConnReq db user cReqSchemas <*> getLinkNotice db ts cReqHashes) >>= \case
(Just _, _) -> plan $ CAPOwnLink
(Nothing, Just (expiresAt, reason)) -> plan $ CAPLinkNotice expiresAt reason
(Nothing, Nothing) ->
(Nothing, notice_) -> do
let okPlan = plan $ maybe (CAPOk cld ov) (`CAPLinkNotice` cld) notice_
withFastStore' (\db -> getContactConnEntityByConnReqHash db cxt user cReqHashes) >>= \case
Nothing ->
withFastStore' (\db -> getContactWithoutConnViaAddress db cxt user cReqSchemas) >>= \case
Just ct | not (contactDeleted ct) -> plan $ CAPContactViaAddress ct
_ -> plan $ CAPOk cld ov
_ -> okPlan
Just (RcvDirectMsgConnection Connection {connStatus} Nothing)
| connStatus == ConnPrepared -> plan $ CAPOk cld ov
| connStatus == ConnPrepared -> okPlan
| otherwise -> plan CAPConnectingConfirmReconnect
Just (RcvDirectMsgConnection _ (Just ct))
| not (contactReady ct) && contactActive ct -> plan $ CAPConnectingProhibit ct
| contactDeleted ct -> plan $ CAPOk cld ov
| contactDeleted ct -> okPlan
| otherwise -> plan $ CAPKnown ct
-- TODO [short links] RcvGroupMsgConnection branch is deprecated? (old group link protocol?)
Just (RcvGroupMsgConnection _ gInfo _) -> groupPlan gInfo Nothing Nothing Nothing []
Just (RcvGroupMsgConnection _ gInfo _) -> groupPlan gInfo notice_ Nothing Nothing Nothing []
Just _ -> throwCmdError "found connection entity is not RcvDirectMsgConnection or RcvGroupMsgConnection"
groupJoinRequestPlan :: User -> ConnReqContact -> Maybe GroupShortLinkInfo -> Maybe GroupShortLinkData -> Maybe OwnerVerification -> [GroupLinkOwner] -> CM ConnectionPlan
groupJoinRequestPlan user cReq linkInfo gld ov glOwners = do
@@ -4785,28 +4796,29 @@ processChatCommand cxt nm = \case
ts <- liftIO getCurrentTime
withFastStore' (\db -> (,) <$> getGroupInfoByUserContactLinkConnReq db cxt user cReqSchemas <*> getLinkNotice db ts cReqHashes) >>= \case
(Just g, _) -> plan $ GLPOwnLink g
(Nothing, Just (expiresAt, reason)) -> plan $ GLPLinkNotice expiresAt reason
(Nothing, Nothing) -> do
(Nothing, notice_) -> do
connEnt_ <- withFastStore' $ \db -> getContactConnEntityByConnReqHash db cxt user cReqHashes
gInfo_ <- withFastStore' $ \db -> getGroupInfoByGroupLinkHash db cxt user cReqHashes
case (gInfo_, connEnt_) of
(Nothing, Nothing) -> plan $ GLPOk linkInfo gld ov
(Nothing, Nothing) -> plan $ groupOkPlan notice_ linkInfo gld ov
-- TODO [short links] RcvDirectMsgConnection branches are deprecated? (old group link protocol?)
(Nothing, Just (RcvDirectMsgConnection _conn Nothing)) -> plan $ GLPConnectingConfirmReconnect
(Nothing, Just (RcvDirectMsgConnection _ (Just ct)))
| not (contactReady ct) && contactActive ct -> plan $ GLPConnectingProhibit gInfo_
| otherwise -> plan $ GLPOk linkInfo gld ov
| otherwise -> plan $ groupOkPlan notice_ linkInfo gld ov
(Nothing, Just _) -> throwCmdError "found connection entity is not RcvDirectMsgConnection"
(Just gInfo, _) -> groupPlan gInfo linkInfo gld ov glOwners
groupPlan :: GroupInfo -> Maybe GroupShortLinkInfo -> Maybe GroupShortLinkData -> Maybe OwnerVerification -> [GroupLinkOwner] -> CM ConnectionPlan
groupPlan gInfo@GroupInfo {membership} linkInfo gld ov glOwners
(Just gInfo, _) -> groupPlan gInfo notice_ linkInfo gld ov glOwners
groupPlan :: GroupInfo -> Maybe (Maybe UTCTime) -> Maybe GroupShortLinkInfo -> Maybe GroupShortLinkData -> Maybe OwnerVerification -> [GroupLinkOwner] -> CM ConnectionPlan
groupPlan gInfo@GroupInfo {membership} notice_ linkInfo gld ov glOwners
| memberStatus membership == GSMemRejected = plan $ GLPKnown gInfo False ov (ListDef glOwners)
| not (memberActive membership) && not (memberRemoved membership) =
plan $ GLPConnectingProhibit $ Just gInfo
| memberActive membership = plan $ GLPKnown gInfo False ov (ListDef glOwners)
| otherwise = plan $ GLPOk linkInfo gld ov
| otherwise = plan $ groupOkPlan notice_ linkInfo gld ov
where
plan p = pure $ CPGroupLink p
groupOkPlan :: Maybe (Maybe UTCTime) -> Maybe GroupShortLinkInfo -> Maybe GroupShortLinkData -> Maybe OwnerVerification -> GroupLinkPlan
groupOkPlan notice_ linkInfo gld ov = maybe (GLPOk linkInfo gld ov) GLPLinkNotice notice_
contactCReqSchemas :: ConnReqContact -> (ConnReqContact, ConnReqContact)
contactCReqSchemas (CRContactUri crData e2e) =
( CRContactUri crData {crScheme = SSSimplex} e2e,
@@ -4818,7 +4830,7 @@ processChatCommand cxt nm = \case
checkLinkNotice cReq = do
ts <- liftIO getCurrentTime
withFastStore' (\db -> getLinkNotice db ts $ contactCReqHashes cReq)
>>= mapM_ (\(expiresAt, reason) -> throwChatError $ CELinkNotice expiresAt reason)
>>= mapM_ (throwChatError . CELinkNotice)
-- This function is needed, as UI uses simplex:/ schema in message view, so that the links can be handled without browser,
-- and short links are stored with server hostname schema, so they wouldn't match without it.
serverShortLink :: ConnShortLink m -> ConnShortLink m
@@ -6087,6 +6099,35 @@ agentSubscriber = do
type AgentSubResult = Map ConnId (Either AgentErrorType ())
blockMembers :: User -> GroupInfoKeys -> MemberRestrictions -> [GroupMember] -> [GroupMember] -> CM ([GroupMember], Bool)
blockMembers user g@(GIK gInfo _) mrs@MemberRestrictions {restriction} blockMems remainingMems = case L.nonEmpty blockMems of
Nothing -> throwCmdError "no members to block/unblock"
Just blockMems' -> do
let events = L.map (\GroupMember {memberId} -> XGrpMemRestrict memberId mrs) blockMems'
(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
when (length cis_ /= length blockMems) $ logError "blockMembers: blockMems and cis_ length mismatch"
let acis = map (AChatItem SCTGroup SMDSnd (GroupChat gInfo Nothing)) $ rights cis_
unless (null acis) $ toView $ CEvtNewChatItems user acis
(errs, blocked) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (updateGroupMemberBlocked db user gInfo mrs True) blockMems)
unless (null errs) $ toView $ CEvtChatErrors errs
-- TODO not batched - requires agent batch api
forM_ blocked $ \m -> toggleNtf m (not blockFlag)
pure (blocked, msgSigned)
where
blockFlag = mrsBlocked restriction
recipients = case blockMems of
[GroupMember {memberStatus = GSMemPendingApproval}] -> []
[GroupMember {memberStatus = GSMemPendingReview}] -> filter (\m -> memberRole' m >= GRModerator && memberCurrent m) remainingMems
_ -> filter memberCurrent remainingMems
sndItemData :: GroupMember -> SndMessage -> NewSndChatItemData c
sndItemData GroupMember {groupMemberId, memberProfile} msg =
let content = CISndGroupEvent $ SGEMemberBlocked groupMemberId (fromLocalProfile memberProfile) blockFlag
ts = ciContentTexts content
in NewSndChatItemData msg content ts M.empty Nothing Nothing Nothing
cleanupManager :: CM ()
cleanupManager = do
interval <- asks (cleanupManagerInterval . config)
@@ -6128,6 +6169,8 @@ cleanupManager = do
liftIO $ threadDelay' stepDelay
cleanupRemovedMembers user `catchAllErrors` eToView
liftIO $ threadDelay' stepDelay
cleanupExpiredBlocks user `catchAllErrors` eToView
liftIO $ threadDelay' stepDelay
cleanupTimedItems cleanupInterval user = do
ts <- liftIO getCurrentTime
let startTimedThreadCutoff = addUTCTime cleanupInterval ts
@@ -6161,6 +6204,27 @@ cleanupManager = do
removedMembers <- withStore' $ \db -> getRemovedMembersToCleanup db cxt user cutoffTs
forM_ removedMembers $ \m ->
withStore' (\db -> deleteGroupMember db user m) `catchAllErrors` eToView
cleanupExpiredBlocks user = do
cxt <- chatStoreCxt
ts <- liftIO getCurrentTime
expiredBlocks <- withStore' $ \db -> getExpiredBlocks db user ts
forM_ expiredBlocks $ \(groupId, gmId, byUser) ->
withGroupLock "blockForAll" groupId (unblockExpired cxt ts groupId gmId byUser) `catchAllErrors` eToView
where
unblockExpired cxt ts groupId gmId byUser = do
(gInfo@GroupInfo {membership}, m) <- withStore $ \db -> (,) <$> getGroupInfo db cxt user groupId <*> getGroupMember db cxt user groupId gmId
when (maybe False (<= ts) (blockedUntil m)) $ do
let mrs = MemberRestrictions {restriction = MRSUnrestricted, expiresAt = blockedUntil m}
ms' <-
if byUser && memberActive membership && memberRole' membership >= max GRModerator (memberRole' m)
then do
(Group gInfo' members, gks) <- withStore $ \db -> getGroupKeys_ db cxt user groupId
fst <$> blockMembers user (GIK gInfo' gks) mrs [m] (filter ((gmId /=) . groupMemberId') members)
else do
m' <- withStore' $ \db -> updateGroupMemberBlocked db user gInfo mrs False m
toggleNtf m' True
pure [m']
forM_ ms' $ toView . CEvtGroupMemberUpdated user gInfo m
cleanupMessages = do
ts <- liftIO getCurrentTime
let cutoffTs = addUTCTime (-(30 * nominalDay)) ts
@@ -6445,7 +6509,7 @@ chatCommandP =
"/_accept member #" *> (APIAcceptMember <$> A.decimal <* A.space <*> A.decimal <*> memberRole),
"/_delete member chat #" *> (APIDeleteMemberSupportChat <$> A.decimal <* A.space <*> A.decimal),
"/_member role #" *> (APIMembersRole <$> A.decimal <*> _strP <*> memberRole),
"/_block #" *> (APIBlockMembersForAll <$> A.decimal <*> _strP <* " blocked=" <*> onOffP),
"/_block #" *> (APIBlockMembersForAll <$> A.decimal <*> _strP <* " blocked=" <*> onOffP <*> optional (" ttl=" *> A.decimal)),
"/_remove #" *> (APIRemoveMembers <$> A.decimal <*> _strP <*> (" messages=" *> onOffP <|> pure False) <*> optional (" notice=" *> jsonP)),
"/_leave #" *> (APILeaveGroup <$> A.decimal),
"/_members #" *> (APIListMembers <$> A.decimal),
@@ -6544,8 +6608,8 @@ chatCommandP =
("/join " <|> "/j ") *> char_ '#' *> (JoinGroup <$> displayNameP <*> (" mute" $> MFNone <|> pure MFAll)),
"/accept member " *> char_ '#' *> (AcceptMember <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <*> (memberRole <|> pure GRMember)),
("/member role " <|> "/mr ") *> char_ '#' *> (MemberRole <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <*> memberRole),
"/block for all #" *> (BlockForAll <$> displayNameP <* A.space <*> (char_ '@' *> displayNameP) <*> pure True),
"/unblock for all #" *> (BlockForAll <$> displayNameP <* A.space <*> (char_ '@' *> displayNameP) <*> pure False),
"/block for all #" *> (BlockForAll <$> displayNameP <* A.space <*> (char_ '@' *> displayNameP) <*> pure True <*> optional (" ttl=" *> A.decimal)),
"/unblock for all #" *> (BlockForAll <$> displayNameP <* A.space <*> (char_ '@' *> displayNameP) <*> pure False <*> pure Nothing),
("/remove " <|> "/rm ") *> char_ '#' *> (RemoveMembers <$> displayNameP <* A.space <*> (L.fromList <$> (char_ '@' *> displayNameP) `A.sepBy1'` A.char ',') <*> (" messages=" *> onOffP <|> pure False) <*> optional (" notice=" *> jsonP)),
("/leave " <|> "/l ") *> char_ '#' *> (LeaveGroup <$> displayNameP),
("/delete #" <|> "/d #") *> (DeleteGroup <$> displayNameP),
@@ -6747,6 +6811,7 @@ chatCommandP =
[ " full" *> (CDMFull <$> notifyP),
" entity" *> (CDMEntity <$> notifyP),
" messages" $> CDMMessages,
" archive" $> CDMArchive,
CDMFull <$> notifyP -- backwards compatible
]
where
+88 -66
View File
@@ -1084,7 +1084,7 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
where
MsgContainer {scope, asGroup} = mc
-- file description is always allowed, to allow sending files to support scope
XMsgFileDescr sharedMsgId fileDescr fileExpires fileBadge -> groupMessageFileDescription gInfo' (Just m'') sharedMsgId fileDescr fileExpires fileBadge
XMsgFileDescr sharedMsgId fileDescr fileExpires fileBadge -> groupMessageFileDescription gInfo' (Just m'') sharedMsgId fileDescr fileExpires fileBadge msg
XMsgUpdate sharedMsgId mContent mentions ttl live msgScope asGroup_ ->
checkSendAsGroup asGroup_ $
memberCanSend (Just m'') msgScope $
@@ -1111,11 +1111,11 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
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 notice_ -> case encoding @e of
SJson -> fmap ctx <$> xGrpMemDel (GIK gInfo' gks) Nothing m'' memId withMessages rosterVer notice_ verifiedMsg msg brokerTs False
XGrpMemDel memId withMessages rosterVer silent notice_ -> case encoding @e of
SJson -> fmap ctx <$> xGrpMemDel (GIK gInfo' gks) Nothing m'' memId withMessages rosterVer silent notice_ verifiedMsg msg brokerTs False
SBinary -> pure Nothing
XGrpLeave -> fmap ctx <$> xGrpLeave (GIK gInfo' gks) m'' msg brokerTs
XGrpDel notice_ -> Just (DeliveryTaskContext (DJSGroup {jobSpec = DJRelayRemoved}) False) <$ xGrpDel gInfo' m'' notice_ msg brokerTs
XGrpDel -> Just (DeliveryTaskContext (DJSGroup {jobSpec = DJRelayRemoved}) False) <$ xGrpDel gInfo' m'' 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
@@ -1719,6 +1719,12 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
createInternalChatItem user (CDGroupRcv gInfo' scopeInfo mem') (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing
toView $ CEvtAcceptingGroupJoinRequestMember user gInfo' mem'
blockedForUser :: GroupInfo -> GroupMember -> Bool
blockedForUser GroupInfo {membership} m = blockedByAdmin m && memberRole' membership < GRModerator
deleteRcvMsg :: RcvMessage -> CM ()
deleteRcvMsg RcvMessage {msgId} = withStore' (`deleteRcvMessage` msgId)
muteEventInChannel :: GroupInfo -> GroupMember -> Bool
muteEventInChannel gInfo@GroupInfo {membership} m =
useRelays' gInfo
@@ -1975,22 +1981,24 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
binding_ <- if isJust fileBadge then directChatBinding ct else pure Nothing
processFDMessage binding_ fileId aci fileDescr fileExpires fileBadge
groupMessageFileDescription :: GroupInfo -> Maybe GroupMember -> SharedMsgId -> FileDescr -> Maybe UTCTime -> Maybe BadgeProof -> CM (Maybe DeliveryTaskContext)
groupMessageFileDescription g@GroupInfo {groupId} m_ sharedMsgId fileDescr fileExpires fileBadge = do
(fileId, aci) <- withStore $ \db -> do
fileId <- getGroupFileIdBySharedMsgId db userId groupId sharedMsgId
aci <- getChatItemByFileId db cxt user fileId
pure (fileId, aci)
case aci of
AChatItem SCTGroup SMDRcv (GroupChat _g scopeInfo) ChatItem {chatDir, meta = CIMeta {showGroupAsSender}}
| validSender m_ chatDir -> do
-- in processFDMessage some paths are programmed as errors,
-- for example failure on not approved relays (CEFileNotApproved).
-- we catch error, so that even if processFDMessage fails, message can still be forwarded.
processFDMessage (rcvGroupChatBinding g m_ showGroupAsSender fileBadge) fileId aci fileDescr fileExpires fileBadge `catchAllErrors` \_ -> pure ()
pure $ Just $ infoToDeliveryContext g scopeInfo (isChannelDir chatDir)
| otherwise -> messageError "x.msg.file.descr: file/sender mismatch" $> Nothing
_ -> messageError "x.msg.file.descr: invalid file description part" $> Nothing
groupMessageFileDescription :: GroupInfo -> Maybe GroupMember -> SharedMsgId -> FileDescr -> Maybe UTCTime -> Maybe BadgeProof -> RcvMessage -> CM (Maybe DeliveryTaskContext)
groupMessageFileDescription g@GroupInfo {groupId} m_ sharedMsgId fileDescr fileExpires fileBadge msg
| maybe False (blockedForUser g) m_ = Nothing <$ deleteRcvMsg msg
| otherwise = do
(fileId, aci) <- withStore $ \db -> do
fileId <- getGroupFileIdBySharedMsgId db userId groupId sharedMsgId
aci <- getChatItemByFileId db cxt user fileId
pure (fileId, aci)
case aci of
AChatItem SCTGroup SMDRcv (GroupChat _g scopeInfo) ChatItem {chatDir, meta = CIMeta {showGroupAsSender}}
| validSender m_ chatDir -> do
-- in processFDMessage some paths are programmed as errors,
-- for example failure on not approved relays (CEFileNotApproved).
-- we catch error, so that even if processFDMessage fails, message can still be forwarded.
processFDMessage (rcvGroupChatBinding g m_ showGroupAsSender fileBadge) fileId aci fileDescr fileExpires fileBadge `catchAllErrors` \_ -> pure ()
pure $ Just $ infoToDeliveryContext g scopeInfo (isChannelDir chatDir)
| otherwise -> messageError "x.msg.file.descr: file/sender mismatch" $> Nothing
_ -> messageError "x.msg.file.descr: invalid file description part" $> Nothing
processFDMessage :: Maybe ByteString -> FileTransferId -> AChatItem -> FileDescr -> Maybe UTCTime -> Maybe BadgeProof -> CM ()
processFDMessage binding_ fileId aci fileDescr fileExpires fileBadge = do
@@ -2213,6 +2221,7 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
-- no delivery task - message already forwarded by relay
pure Nothing
Just m@GroupMember {memberId}
| blockedForUser gInfo m -> Nothing <$ deleteRcvMsg msg
-- only an owner may post as the channel; a non-owner's signed asGroup post (e.g. relay-injected) must not render as the channel
| sentAsGroup && memberRole' m < GROwner ->
messageError "x.msg.new: member is not allowed to send as group" $> Nothing
@@ -2305,9 +2314,11 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
-- Chat item and update message which created it will have different sharedMsgId in this case...
let timed_ = rcvGroupCITimed gInfo ttl_
showGroupAsSender = fromMaybe (isNothing m_) asGroup_
if showGroupAsSender && maybe False (\m -> memberRole' m < GROwner) m_
then messageError "x.msg.update: member attempted to update as group" $> Nothing
else do
if
| maybe False (blockedForUser gInfo) m_ -> Nothing <$ deleteRcvMsg msg
| showGroupAsSender && maybe False (\m -> memberRole' m < GROwner) m_ ->
messageError "x.msg.update: member attempted to update as group" $> Nothing
| otherwise -> do
(gInfo', chatDir, mentions', scopeInfo) <-
if showGroupAsSender
then pure (gInfo, CDChannelRcv gInfo Nothing, mentions, Nothing)
@@ -2520,18 +2531,20 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
-- TODO remove once XFile is discontinued
processGroupFileInvitation' :: GroupInfo -> GroupMember -> FileInvitation -> RcvMessage -> UTCTime -> CM ()
processGroupFileInvitation' gInfo m fInv msg@RcvMessage {sharedMsgId_} brokerTs = do
ChatConfig {fileChunkSize} <- asks config
fInv'@FileInvitation {fileName, fileSize} <- validateFileInvitation fInv
fileProhibited <- rcvGroupFileProhibited gInfo (Just m) False fInv'
inline <- receiveInlineMode fInv' Nothing fileChunkSize
RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvGroupFileTransfer db userId gInfo (Just m) FTNormal sharedMsgId_ fInv' fileProhibited inline fileChunkSize
let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP
ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol, fileExpires = Nothing, fileProhibited}
content = ciContentNoParse $ CIRcvMsgContent $ MCFile ""
(ci, cInfo) <- saveRcvChatItem' user (CDGroupRcv gInfo Nothing m) msg sharedMsgId_ brokerTs content ciFile Nothing False M.empty
ci' <- blockedMemberCI gInfo m ci
groupMsgToView cInfo ci'
processGroupFileInvitation' gInfo m fInv msg@RcvMessage {sharedMsgId_} brokerTs
| blockedForUser gInfo m = deleteRcvMsg msg
| otherwise = do
ChatConfig {fileChunkSize} <- asks config
fInv'@FileInvitation {fileName, fileSize} <- validateFileInvitation fInv
fileProhibited <- rcvGroupFileProhibited gInfo (Just m) False fInv'
inline <- receiveInlineMode fInv' Nothing fileChunkSize
RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvGroupFileTransfer db userId gInfo (Just m) FTNormal sharedMsgId_ fInv' fileProhibited inline fileChunkSize
let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP
ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol, fileExpires = Nothing, fileProhibited}
content = ciContentNoParse $ CIRcvMsgContent $ MCFile ""
(ci, cInfo) <- saveRcvChatItem' user (CDGroupRcv gInfo Nothing m) msg sharedMsgId_ brokerTs content ciFile Nothing False M.empty
ci' <- blockedMemberCI gInfo m ci
groupMsgToView cInfo ci'
blockedMemberCI :: GroupInfo -> GroupMember -> ChatItem 'CTGroup 'MDRcv -> CM (ChatItem 'CTGroup 'MDRcv)
blockedMemberCI gInfo m ci
@@ -3692,10 +3705,10 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
xGrpMemRestrict :: GroupInfo -> GroupMember -> MemberId -> MemberRestrictions -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope)
xGrpMemRestrict
gInfo@GroupInfo {membership = GroupMember {memberId = membershipMemId}}
gInfo@GroupInfo {membership = GroupMember {memberId = membershipMemId, memberRole = userRole}}
m@GroupMember {memberRole = senderRole}
memId
MemberRestrictions {restriction}
mrs@MemberRestrictions {restriction, expiresAt}
msg@RcvMessage {msgSigned}
brokerTs
| membershipMemId == memId = pure Nothing -- ignore - XGrpMemRestrict can be sent to restricted member for efficiency
@@ -3706,22 +3719,24 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
Just (bm, unknown) -> do
let GroupMember {groupMemberId = bmId, memberRole, blockedByAdmin, memberProfile = bmp} = bm
if
| blockedByAdmin == mrsBlocked restriction -> pure Nothing
| senderRole < GRModerator || senderRole < memberRole ->
messageError "x.grp.mem.restrict with insufficient member permissions" $> Nothing
| blockedByAdmin == blocked || automaticUnblock -> pure $ memberEventDeliveryScope bm
| otherwise -> do
bm' <- setMemberBlocked bm
toggleNtf bm' (not blocked)
let ciContent = CIRcvGroupEvent $ RGEMemberBlocked bmId (fromLocalProfile bmp) blocked
(gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m
(ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv gInfo' scopeInfo m') msg brokerTs ciContent
when (userRole >= GRModerator) $ do
(ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv gInfo' scopeInfo m') msg brokerTs ciContent
groupMsgToView cInfo ci
when unknown $ toView $ CEvtUnknownMemberBlocked user gInfo m bm'
groupMsgToView cInfo ci
toView CEvtMemberBlockedForAll {user, groupInfo = gInfo', byMember = m', member = bm', blocked, msgSigned}
pure $ memberEventDeliveryScope bm
where
setMemberBlocked bm = withStore' $ \db -> updateGroupMemberBlocked db user gInfo restriction bm
setMemberBlocked bm = withStore' $ \db -> updateGroupMemberBlocked db user gInfo mrs False bm
blocked = mrsBlocked restriction
automaticUnblock = not blocked && isJust expiresAt
xGrpMemCon :: GroupInfo -> GroupMember -> MemberId -> CM ()
xGrpMemCon gInfo sendingMem memId = do
@@ -3730,23 +3745,28 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
withStore $ \db -> setMemberVectorRelationConnected db sendingMem refMem MRSubjectConnected
withStore $ \db -> setMemberVectorRelationConnected db refMem sendingMem MRReferencedConnected
xGrpMemDel :: GroupInfoKeys -> Maybe GroupMember -> GroupMember -> MemberId -> Bool -> Maybe VersionRoster -> Maybe LinkNotice -> VerifiedMsg 'Json -> RcvMessage -> UTCTime -> Bool -> CM (Maybe DeliveryJobScope)
xGrpMemDel g@(GIK gInfo@GroupInfo {membership} gks) fwdRelay_ m@GroupMember {memberRole = senderRole} memId withMessages rosterVer_ notice_ verifiedMsg msg@RcvMessage {msgSigned} brokerTs forwarded = do
xGrpMemDel :: GroupInfoKeys -> Maybe GroupMember -> GroupMember -> MemberId -> Bool -> Maybe VersionRoster -> Bool -> Maybe LinkNotice -> VerifiedMsg 'Json -> RcvMessage -> UTCTime -> Bool -> CM (Maybe DeliveryJobScope)
xGrpMemDel g@(GIK gInfo@GroupInfo {membership} gks) fwdRelay_ m@GroupMember {memberRole = senderRole} memId withMessages rosterVer_ silent notice_ verifiedMsg msg@RcvMessage {msgSigned} brokerTs forwarded = do
let GroupMember {memberId = membershipMemId} = membership
trusted = not forwarded || (useRelays' gInfo && msgSigned == Just MSSVerified)
silent' = trusted && silent
if membershipMemId == memId
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
withStore' $ \db -> do
updateGroupMemberStatus db userId membership GSMemRemoved
when (maybe False (/= RSRejected) (relayOwnStatus gInfo)) $ updateRelayOwnStatus_ db gInfo RSInactive
forM_ notice_ $ setGroupLinkNotice db gInfo brokerTs
let membership' = membership {memberStatus = GSMemRemoved}
when withMessages $ deleteMessages gInfo membership'
deleteMemberItem msg gInfo RGEUserDeleted
toView $ CEvtDeletedMemberUser user gInfo {membership = membership'} m withMessages msgSigned
pure $ Just DJSGroup {jobSpec = DJRelayRemoved}
when trusted $ forM_ notice_ $ \notice -> withStore' $ \db -> setGroupLinkNotice db gInfo brokerTs notice
if silent' && not (isUserGrpFwdRelay gInfo)
then Nothing <$ (mapM_ deleteMemberConnection =<< getGroupConnMembers user gInfo)
else do
-- TODO [relays] possible improvement is to immediately delete rcv queues if isUserGrpFwdRelay
unless (isUserGrpFwdRelay gInfo) $ deleteGroupConnections user gInfo False
withStore' $ \db -> do
updateGroupMemberStatus db userId membership GSMemRemoved
when (maybe False (/= RSRejected) (relayOwnStatus gInfo)) $ updateRelayOwnStatus_ db gInfo RSInactive
let membership' = membership {memberStatus = GSMemRemoved}
when withMessages $ deleteMessages gInfo membership'
deleteMemberItem msg gInfo RGEUserDeleted
toView $ CEvtDeletedMemberUser user gInfo {membership = membership'} m withMessages msgSigned
pure $ Just DJSGroup {jobSpec = DJRelayRemoved}
else applyAtRosterVersion g fwdRelay_ m rosterVer_ $
withStore' (\db -> runExceptT $ getGroupMemberByMemberId db cxt user gInfo memId) >>= \case
Left _ -> do
@@ -3779,7 +3799,7 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
-- as the member record was already deleted above.
let RcvMessage {forwardedByMember = fwdBy} = msg
msg' = if fwdBy == Just groupMemberId then (msg :: RcvMessage) {forwardedByMember = Nothing} else msg
unless wasDeleted $ deleteMemberItem msg' gInfo'' $ RGEMemberDeleted groupMemberId (fromLocalProfile memberProfile)
unless (wasDeleted || silent') $ deleteMemberItem msg' gInfo'' $ RGEMemberDeleted groupMemberId (fromLocalProfile memberProfile)
toView $ CEvtDeletedMember user gInfo'' m deletedMember' withMessages msgSigned
pure deliveryScope
where
@@ -3828,13 +3848,11 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
toView $ CEvtLeftMember user gInfo''' m' {memberStatus = GSMemLeft} msgSigned
pure $ memberEventDeliveryScope m
xGrpDel :: GroupInfo -> GroupMember -> Maybe LinkNotice -> RcvMessage -> UTCTime -> CM ()
xGrpDel gInfo@GroupInfo {membership} m@GroupMember {memberRole} notice_ msg@RcvMessage {msgSigned} brokerTs = do
xGrpDel :: GroupInfo -> GroupMember -> RcvMessage -> UTCTime -> CM ()
xGrpDel gInfo@GroupInfo {membership} m@GroupMember {memberRole} msg@RcvMessage {msgSigned} brokerTs = do
when (memberRole /= GROwner) $ throwChatError $ CEGroupUserRole gInfo GROwner
deleteGroupLinkIfExists user gInfo
withStore' $ \db -> do
updateGroupMemberStatus db userId membership GSMemGroupDeleted
forM_ notice_ $ setGroupLinkNotice db gInfo brokerTs
withStore' $ \db -> updateGroupMemberStatus db userId membership GSMemGroupDeleted
-- TODO [relays] possible improvement is to immediately delete rcv queues if isUserGrpFwdRelay
unless (isUserGrpFwdRelay gInfo) $ deleteGroupConnections user gInfo False
(gInfo'', m', scopeInfo) <- mkGroupChatScope gInfo m
@@ -4049,7 +4067,7 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
where
MsgContainer {scope} = mc
-- file description is always allowed, to allow sending files to support scope
XMsgFileDescr sharedMsgId fileDescr fileExpires fileBadge -> void $ groupMessageFileDescription gInfo author_ sharedMsgId fileDescr fileExpires fileBadge
XMsgFileDescr sharedMsgId fileDescr fileExpires fileBadge -> void $ groupMessageFileDescription gInfo author_ sharedMsgId fileDescr fileExpires fileBadge rcvMsg
XMsgUpdate sharedMsgId mContent mentions ttl live msgScope asGroup_ ->
void $ memberCanSend author_ msgScope $ groupMessageUpdate gInfo author_ sharedMsgId mContent mentions msgScope rcvMsg msgTs ttl live asGroup_
XMsgDel sharedMsgId memId scope_ _ -> void $ groupMessageDelete gInfo author_ sharedMsgId memId scope_ False rcvMsg msgTs
@@ -4060,9 +4078,9 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
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 notice_ -> withAuthor XGrpMemDel_ $ \author -> void $ xGrpMemDel g (Just m) author memId withMessages rosterVer notice_ verifiedMsg rcvMsg msgTs True
XGrpMemDel memId withMessages rosterVer silent notice_ -> withAuthor XGrpMemDel_ $ \author -> void $ xGrpMemDel g (Just m) author memId withMessages rosterVer silent notice_ verifiedMsg rcvMsg msgTs True
XGrpLeave -> withAuthor XGrpLeave_ $ \author -> void $ xGrpLeave g author rcvMsg msgTs
XGrpDel notice_ -> withAuthor XGrpDel_ $ \author -> void $ xGrpDel gInfo author notice_ rcvMsg msgTs
XGrpDel -> withAuthor XGrpDel_ $ \author -> void $ xGrpDel gInfo author 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
@@ -4179,11 +4197,15 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
_ -> pure Nothing
deleteGroupConnections :: User -> GroupInfo -> Bool -> CM ()
deleteGroupConnections user gInfo@GroupInfo {membership} waitDelivery = do
cxt <- chatStoreCxt
deleteGroupConnections user gInfo waitDelivery = do
-- member records are not deleted to keep history
members <- getMembers cxt
members <- getGroupConnMembers user gInfo
deleteMembersConnections' user members waitDelivery
getGroupConnMembers :: User -> GroupInfo -> CM [GroupMember]
getGroupConnMembers user gInfo@GroupInfo {membership} = do
cxt <- chatStoreCxt
getMembers cxt
where
getMembers cxt
| useRelays' gInfo, not (isRelay membership) = withStore' $ \db -> getGroupRelayMembers db cxt user gInfo
+9 -15
View File
@@ -300,7 +300,6 @@ instance StrEncoding ReportReason where
A.takeTill (== ' ') >>= \case
"spam" -> pure RRSpam
"content" -> pure RRContent
"illegal" -> pure RRContent
"community" -> pure RRCommunity
"profile" -> pure RRProfile
"other" -> pure RROther
@@ -313,13 +312,8 @@ instance ToJSON ReportReason where
toJSON = strToJSON
toEncoding = strToJEncoding
instance ToField ReportReason where toField = toField . safeDecodeUtf8 . strEncode
instance FromField ReportReason where fromField = fromTextField_ $ eitherToMaybe . strDecode . encodeUtf8
data LinkNotice = LinkNotice
{ ttl :: Maybe Int64,
reason :: Maybe ReportReason
{ ttl :: Maybe Int64
}
deriving (Eq, Show)
@@ -510,9 +504,9 @@ data ChatMsgEvent (e :: MsgEncoding) where
XGrpMemRestrict :: MemberId -> MemberRestrictions -> ChatMsgEvent 'Json
XGrpMemCon :: MemberId -> ChatMsgEvent 'Json
XGrpMemConAll :: MemberId -> ChatMsgEvent 'Json -- TODO not implemented
XGrpMemDel :: MemberId -> Bool -> Maybe VersionRoster -> Maybe LinkNotice -> ChatMsgEvent 'Json
XGrpMemDel :: MemberId -> Bool -> Maybe VersionRoster -> Bool -> Maybe LinkNotice -> ChatMsgEvent 'Json
XGrpLeave :: ChatMsgEvent 'Json
XGrpDel :: Maybe LinkNotice -> ChatMsgEvent 'Json
XGrpDel :: ChatMsgEvent 'Json
XGrpInfo :: GroupProfile -> ChatMsgEvent 'Json
XGrpPrefs :: GroupPreferences -> ChatMsgEvent 'Json
XGrpDirectInv :: ConnReqInvitation -> Maybe MsgContent -> Maybe MsgScope -> ChatMsgEvent 'Json
@@ -559,7 +553,7 @@ isForwardedGroupMsg ev = case ev of
XGrpMemRestrict {} -> True
XGrpMemDel {} -> True
XGrpLeave -> True
XGrpDel _ -> True
XGrpDel -> True
XGrpInfo _ -> True
XGrpPrefs _ -> True
XGrpRoster _ -> True
@@ -1316,7 +1310,7 @@ toCMEventTag msg = case msg of
XGrpMemConAll _ -> XGrpMemConAll_
XGrpMemDel {} -> XGrpMemDel_
XGrpLeave -> XGrpLeave_
XGrpDel _ -> XGrpDel_
XGrpDel -> XGrpDel_
XGrpInfo _ -> XGrpInfo_
XGrpPrefs _ -> XGrpPrefs_
XGrpDirectInv {} -> XGrpDirectInv_
@@ -1492,9 +1486,9 @@ appJsonToCM fwdDepth AppMessageJson {v, msgId, event, params} = do
XGrpMemRestrict_ -> XGrpMemRestrict <$> p "memberId" <*> p "memberRestrictions"
XGrpMemCon_ -> XGrpMemCon <$> p "memberId"
XGrpMemConAll_ -> XGrpMemConAll <$> p "memberId"
XGrpMemDel_ -> XGrpMemDel <$> p "memberId" <*> Right (fromRight False $ p "messages") <*> opt "rosterVersion" <*> Right (fromRight Nothing $ opt "notice")
XGrpMemDel_ -> XGrpMemDel <$> p "memberId" <*> Right (fromRight False $ p "messages") <*> opt "rosterVersion" <*> Right (fromRight False $ p "silent") <*> Right (fromRight Nothing $ opt "notice")
XGrpLeave_ -> pure XGrpLeave
XGrpDel_ -> XGrpDel <$> Right (fromRight Nothing $ opt "notice")
XGrpDel_ -> pure XGrpDel
XGrpInfo_ -> XGrpInfo <$> p "groupProfile"
XGrpPrefs_ -> XGrpPrefs <$> p "groupPreferences"
XGrpDirectInv_ -> XGrpDirectInv <$> p "connReq" <*> opt "content" <*> opt "scope"
@@ -1568,9 +1562,9 @@ chatToAppMessage chatMsg@ChatMessage {chatVRange, msgId, chatMsgEvent} = case en
XGrpMemRestrict memId memRestrictions -> o ["memberId" .= memId, "memberRestrictions" .= memRestrictions]
XGrpMemCon memId -> o ["memberId" .= memId]
XGrpMemConAll memId -> o ["memberId" .= memId]
XGrpMemDel memId messages rosterVersion notice -> o $ ("notice" .=? notice) $ ("rosterVersion" .=? rosterVersion) $ ("messages" .=? if messages then Just True else Nothing) ["memberId" .= memId]
XGrpMemDel memId messages rosterVersion silent notice -> o $ ("notice" .=? notice) $ ("silent" .=? justTrue silent) $ ("rosterVersion" .=? rosterVersion) $ ("messages" .=? if messages then Just True else Nothing) ["memberId" .= memId]
XGrpLeave -> JM.empty
XGrpDel notice -> o $ ("notice" .=? notice) []
XGrpDel -> JM.empty
XGrpInfo p -> o ["groupProfile" .= p]
XGrpPrefs p -> o ["groupPreferences" .= p]
XGrpDirectInv connReq content scope -> o $ ("content" .=? content) $ ("scope" .=? scope) ["connReq" .= connReq]
+2 -2
View File
@@ -164,14 +164,14 @@ getConnectionEntityKeys db cxt user@User {userId, userContactId} agentConnId = d
g.root_priv_key, g.root_pub_key, g.member_priv_key,
-- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
mu.member_status, mu.show_messages, mu.member_restriction, mu.member_restriction_expires_at, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
-- GroupInfo {membership = GroupMember {memberProfile}}
pu.display_name, pu.full_name, pu.short_descr, pu.description, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, pu.preferences_json,
pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified,
mu.created_at, mu.updated_at,
mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link, mu.member_security_code, mu.member_security_code_verified_at,
-- from GroupMember
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction,
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.member_restriction_expires_at,
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.preferences_json,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified,
m.created_at, m.updated_at,
+34 -14
View File
@@ -77,6 +77,7 @@ module Simplex.Chat.Store.Groups
getGroupRelayMembers,
getGroupMembersForExpiration,
getRemovedMembersToCleanup,
getExpiredBlocks,
deleteGroupChatItems,
deleteGroupMembers,
cleanupHostGroupLinkConn,
@@ -266,11 +267,11 @@ import Database.SQLite.Simple (Only (..), Query, (:.) (..))
import Database.SQLite.Simple.QQ (sql)
#endif
type MaybeGroupMemberRow = (Maybe GroupMemberId, Maybe GroupId, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId) :. ((Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe LocalAlias, Maybe Text, Maybe Text) :. BadgeRow :. ContactDomainRow) :. (Maybe UTCTime, Maybe UTCTime) :. (Maybe UTCTime, Maybe Int64, Maybe Int64, Maybe Int64, Maybe UTCTime, Maybe C.PublicKeyEd25519, Maybe ShortLinkContact, Maybe Text, Maybe UTCTime)
type MaybeGroupMemberRow = (Maybe GroupMemberId, Maybe GroupId, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe BoolInt, Maybe MemberRestrictionStatus, Maybe UTCTime) :.(Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId) :. ((Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe LocalAlias, Maybe Text, Maybe Text) :. BadgeRow :. ContactDomainRow) :. (Maybe UTCTime, Maybe UTCTime) :. (Maybe UTCTime, Maybe Int64, Maybe Int64, Maybe Int64, Maybe UTCTime, Maybe C.PublicKeyEd25519, Maybe ShortLinkContact, Maybe Text, Maybe UTCTime)
toMaybeGroupMember :: UTCTime -> Int64 -> MaybeGroupMemberRow -> Maybe GroupMember
toMaybeGroupMember now userContactId ((Just groupMemberId, Just groupId, Just indexInGroup, Just memberId, Just minVer, Just maxVer, Just memberRole, Just memberCategory, Just memberStatus, Just showMessages, memberBlocked') :. (invitedById, invitedByGroupMemberId, Just localDisplayName, memberContactId, Just memberContactProfileId) :. ((Just profileId, Just displayName, Just fullName, shortDescr, description, image, contactLink, peerType, Just localAlias, encodedPrefs, receivedPrefs) :. badgeRow :. domainRow) :. (Just createdAt, Just updatedAt) :. (supportChatTs, Just supportChatUnread, Just supportChatUnanswered, Just supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, relayLink, memberCode_, memberCodeVerifiedAt_)) =
Just $ toGroupMember now userContactId ((groupMemberId, groupId, indexInGroup, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberBlocked') :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId) :. ((profileId, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias, encodedPrefs, receivedPrefs) :. badgeRow :. domainRow) :. (createdAt, updatedAt) :. (supportChatTs, supportChatUnread, supportChatUnanswered, supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, relayLink, memberCode_, memberCodeVerifiedAt_))
toMaybeGroupMember now userContactId ((Just groupMemberId, Just groupId, Just indexInGroup, Just memberId, Just minVer, Just maxVer, Just memberRole, Just memberCategory, Just memberStatus, Just showMessages, memberBlocked', blockedUntil) :.(invitedById, invitedByGroupMemberId, Just localDisplayName, memberContactId, Just memberContactProfileId) :. ((Just profileId, Just displayName, Just fullName, shortDescr, description, image, contactLink, peerType, Just localAlias, encodedPrefs, receivedPrefs) :. badgeRow :. domainRow) :. (Just createdAt, Just updatedAt) :. (supportChatTs, Just supportChatUnread, Just supportChatUnanswered, Just supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, relayLink, memberCode_, memberCodeVerifiedAt_)) =
Just $ toGroupMember now userContactId ((groupMemberId, groupId, indexInGroup, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberBlocked', blockedUntil) :.(invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId) :. ((profileId, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias, encodedPrefs, receivedPrefs) :. badgeRow :. domainRow) :. (createdAt, updatedAt) :. (supportChatTs, supportChatUnread, supportChatUnanswered, supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, relayLink, memberCode_, memberCodeVerifiedAt_))
toMaybeGroupMember _ _ _ = Nothing
createGroupLink :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> ConnId -> CreatedLinkContact -> GroupLinkId -> GroupMemberRole -> SubscriptionMode -> ExceptT StoreError IO GroupLink
@@ -606,6 +607,7 @@ createContactMemberInv_ db User {userId, userContactId} groupId invitedByGroupMe
memberStatus,
memberSettings = defaultMemberSettings,
blockedByAdmin = False,
blockedUntil = Nothing,
invitedBy,
invitedByGroupMemberId,
localDisplayName,
@@ -1381,6 +1383,19 @@ getRemovedMembersToCleanup db cxt user@User {userId} cutoffTs = do
(groupMemberQuery <> " WHERE m.user_id = ? AND m.removed_at < ?")
(userId, cutoffTs)
getExpiredBlocks :: DB.Connection -> User -> UTCTime -> IO [(GroupId, GroupMemberId, Bool)]
getExpiredBlocks db User {userId} now =
map (\(groupId, groupMemberId, BI byUser) -> (groupId, groupMemberId, byUser))
<$> DB.query
db
[sql|
SELECT group_id, group_member_id, member_restriction_by_user
FROM group_members
WHERE user_id = ? AND member_restriction = ? AND member_restriction_expires_at <= ?
AND member_status NOT IN (?,?,?,?)
|]
(userId, MRSBlocked, now, GSMemRejected, GSMemRemoved, GSMemLeft, GSMemGroupDeleted)
getGroupInvitation :: DB.Connection -> StoreCxt -> User -> GroupId -> ExceptT StoreError IO ReceivedGroupInvitation
getGroupInvitation db cxt user groupId =
getConnRec_ user >>= \case
@@ -1422,6 +1437,7 @@ createNewContactMember db gVar User {userId, userContactId} GroupInfo {groupId,
memberStatus = GSMemInvited,
memberSettings = defaultMemberSettings,
blockedByAdmin = False,
blockedUntil = Nothing,
invitedBy = IBUser,
invitedByGroupMemberId = Just invitedByGroupMemberId,
localDisplayName,
@@ -2533,6 +2549,8 @@ createNewMember_
memberChatVRange@(VersionRange minV maxV) = maybe chatInitialVRange fromChatVRange memChatVRange
memberPubKey = (\(MemberKey k) -> k) <$> memKey
memberProof = verifiedProof badgeVerified badge
restriction_ = restriction <$> memRestriction
blockedUntil = memRestriction >>= blockExpiry
indexInGroup <- getUpdateNextIndexInGroup_ db groupId
liftIO $
DB.execute
@@ -2540,13 +2558,13 @@ createNewMember_
[sql|
INSERT INTO group_members
(group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector,
member_restriction, invited_by, invited_by_group_member_id,
member_restriction, member_restriction_expires_at, invited_by, invited_by_group_member_id,
user_id, local_display_name, contact_id, contact_profile_id, member_pub_key, created_at, updated_at,
peer_chat_min_version, peer_chat_max_version)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|]
( (groupId, indexInGroup, memberId, memberRole, memberCategory, memberStatus, Binary B.empty)
:. (memRestriction, invitedById, memInvitedByGroupMemberId)
:. (restriction_, blockedUntil, invitedById, memInvitedByGroupMemberId)
:. (userId, localDisplayName, memberContactId, memberContactProfileId, memberPubKey, createdAt, createdAt)
:. (minV, maxV)
)
@@ -2562,7 +2580,8 @@ createNewMember_
memberCategory,
memberStatus,
memberSettings = defaultMemberSettings,
blockedByAdmin = maybe False mrsBlocked memRestriction,
blockedByAdmin = blockedAt createdAt restriction_ blockedUntil,
blockedUntil,
invitedBy,
invitedByGroupMemberId = memInvitedByGroupMemberId,
localDisplayName,
@@ -2693,8 +2712,7 @@ createIntroReMember
memRestrictions_ = do
currentTs <- liftIO getCurrentTime
(localDisplayName, memProfileId, memberProfile', badgeVerified) <- createNewMemberProfile_ db cxt user memberProfile presHeader_ currentTs
let memRestriction = restriction <$> memRestrictions_
newMember = NewGroupMember {memInfo = (memInfo :: MemberInfo) {profile = memberProfile'}, memCategory = GCPreMember, memStatus = GSMemIntroduced, memRestriction, memInvitedBy = IBUnknown, memInvitedByGroupMemberId = Nothing, localDisplayName, memContactId = Nothing, memProfileId}
let newMember = NewGroupMember {memInfo = (memInfo :: MemberInfo) {profile = memberProfile'}, memCategory = GCPreMember, memStatus = GSMemIntroduced, memRestriction = memRestrictions_, memInvitedBy = IBUnknown, memInvitedByGroupMemberId = Nothing, localDisplayName, memContactId = Nothing, memProfileId}
createNewMember_ db user (groupId' gInfo) newMember badgeVerified currentTs
createIntroReMemberConn :: DB.Connection -> User -> GroupMember -> GroupMember -> VersionChat -> MemberInfo -> (CommandId, ConnId) -> SubscriptionMode -> ExceptT StoreError IO GroupMember
@@ -3223,18 +3241,20 @@ updateGroupMemberSettings db User {userId} gId gMemberId GroupMemberSettings {sh
|]
(BI showMessages, currentTs, userId, gId, gMemberId)
updateGroupMemberBlocked :: DB.Connection -> User -> GroupInfo -> MemberRestrictionStatus -> GroupMember -> IO GroupMember
updateGroupMemberBlocked db User {userId} GroupInfo {groupId} mrs m@GroupMember {groupMemberId} = do
updateGroupMemberBlocked :: DB.Connection -> User -> GroupInfo -> MemberRestrictions -> Bool -> GroupMember -> IO GroupMember
updateGroupMemberBlocked db User {userId} GroupInfo {groupId} mrs@MemberRestrictions {restriction} byUser m@GroupMember {groupMemberId} = do
currentTs <- getCurrentTime
DB.execute
db
[sql|
UPDATE group_members
SET member_restriction = ?, updated_at = ?
SET member_restriction = ?, member_restriction_expires_at = ?, member_restriction_by_user = ?, updated_at = ?
WHERE user_id = ? AND group_id = ? AND group_member_id = ?
|]
(mrs, currentTs, userId, groupId, groupMemberId)
pure m {blockedByAdmin = mrsBlocked mrs}
(restriction, blockedUntil, BI byUser, currentTs, userId, groupId, groupMemberId)
pure m {blockedByAdmin = blockedAt currentTs (Just restriction) blockedUntil, blockedUntil}
where
blockedUntil = blockExpiry mrs
getHostConnId :: DB.Connection -> User -> GroupId -> ExceptT StoreError IO GroupMemberId
getHostConnId db user@User {userId} groupId = do
+8 -4
View File
@@ -35,6 +35,7 @@ module Simplex.Chat.Store.Messages
getPendingGroupMessages,
deletePendingGroupMessage,
deleteOldMessages,
deleteRcvMessage,
MemberAttention (..),
updateChatTsStats,
setSupportChatTs,
@@ -393,6 +394,9 @@ deleteOldMessages :: DB.Connection -> UTCTime -> IO ()
deleteOldMessages db createdAtCutoff = do
DB.execute db "DELETE FROM messages WHERE created_at <= ?" (Only createdAtCutoff)
deleteRcvMessage :: DB.Connection -> MessageId -> IO ()
deleteRcvMessage db msgId = DB.execute db "DELETE FROM messages WHERE message_id = ?" (Only msgId)
type NewQuoteRow = (Maybe SharedMsgId, Maybe UTCTime, Maybe MsgContent, Maybe Bool, Maybe MemberId)
-- For support chats with members we track unanswered count - number of messages from the member
@@ -724,7 +728,7 @@ getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRe
SELECT i.chat_item_id,
-- GroupMember
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category,
m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id,
m.member_status, m.show_messages, m.member_restriction, m.member_restriction_expires_at, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id,
p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.preferences_json,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified,
m.created_at, m.updated_at,
@@ -3119,7 +3123,7 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do
i.forwarded_by_group_member_id, i.show_group_as_sender,
-- GroupMember
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category,
m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id,
m.member_status, m.show_messages, m.member_restriction, m.member_restriction_expires_at, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id,
p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.preferences_json,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified,
m.created_at, m.updated_at,
@@ -3128,14 +3132,14 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do
ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent,
-- quoted GroupMember
rm.group_member_id, rm.group_id, rm.index_in_group, rm.member_id, rm.peer_chat_min_version, rm.peer_chat_max_version, rm.member_role, rm.member_category,
rm.member_status, rm.show_messages, rm.member_restriction, rm.invited_by, rm.invited_by_group_member_id, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id,
rm.member_status, rm.show_messages, rm.member_restriction, rm.member_restriction_expires_at, rm.invited_by, rm.invited_by_group_member_id, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id,
rp.display_name, rp.full_name, rp.short_descr, rp.description, rp.image, rp.contact_link, rp.chat_peer_type, rp.local_alias, rp.preferences, rp.preferences_json,
rp.badge_proof, rp.badge_pres_header, rp.badge_expiry, rp.badge_type, rp.badge_verified, rp.badge_extra, rp.badge_master_key, rp.badge_signature, rp.badge_key_idx, rp.contact_domain, rp.contact_domain_proof, rp.contact_domain_verified,
rm.created_at, rm.updated_at,
rm.support_chat_ts, rm.support_chat_items_unread, rm.support_chat_items_member_attention, rm.support_chat_items_mentions, rm.support_chat_last_msg_from_member_ts, rm.member_pub_key, rm.relay_link, rm.member_security_code, rm.member_security_code_verified_at,
-- deleted by GroupMember
dbm.group_member_id, dbm.group_id, dbm.index_in_group, dbm.member_id, dbm.peer_chat_min_version, dbm.peer_chat_max_version, dbm.member_role, dbm.member_category,
dbm.member_status, dbm.show_messages, dbm.member_restriction, dbm.invited_by, dbm.invited_by_group_member_id, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id,
dbm.member_status, dbm.show_messages, dbm.member_restriction, dbm.member_restriction_expires_at, dbm.invited_by, dbm.invited_by_group_member_id, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id,
dbp.display_name, dbp.full_name, dbp.short_descr, dbp.description, dbp.image, dbp.contact_link, dbp.chat_peer_type, dbp.local_alias, dbp.preferences, dbp.preferences_json,
dbp.badge_proof, dbp.badge_pres_header, dbp.badge_expiry, dbp.badge_type, dbp.badge_verified, dbp.badge_extra, dbp.badge_master_key, dbp.badge_signature, dbp.badge_key_idx, dbp.contact_domain, dbp.contact_domain_proof, dbp.contact_domain_verified,
dbm.created_at, dbm.updated_at,
@@ -13,17 +13,26 @@ CREATE TABLE link_notices(
link_notice_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
link_hash BYTEA NOT NULL,
expires_at TIMESTAMPTZ,
reason TEXT,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
CREATE UNIQUE INDEX idx_link_notices_link_hash ON link_notices(link_hash);
ALTER TABLE group_members ADD COLUMN member_restriction_expires_at TIMESTAMPTZ;
ALTER TABLE group_members ADD COLUMN member_restriction_by_user SMALLINT NOT NULL DEFAULT 0;
CREATE INDEX idx_group_members_member_restriction_expires_at ON group_members(member_restriction_expires_at);
|]
down_m20261020_link_notices :: Text
down_m20261020_link_notices =
[r|
DROP INDEX idx_group_members_member_restriction_expires_at;
ALTER TABLE group_members DROP COLUMN member_restriction_by_user;
ALTER TABLE group_members DROP COLUMN member_restriction_expires_at;
DROP INDEX idx_link_notices_link_hash;
DROP TABLE link_notices;
|]
@@ -12,17 +12,26 @@ CREATE TABLE link_notices(
link_notice_id INTEGER PRIMARY KEY AUTOINCREMENT,
link_hash BLOB NOT NULL,
expires_at TEXT,
reason TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
) STRICT;
CREATE UNIQUE INDEX idx_link_notices_link_hash ON link_notices(link_hash);
ALTER TABLE group_members ADD COLUMN member_restriction_expires_at TEXT;
ALTER TABLE group_members ADD COLUMN member_restriction_by_user INTEGER NOT NULL DEFAULT 0;
CREATE INDEX idx_group_members_member_restriction_expires_at ON group_members(member_restriction_expires_at);
|]
down_m20261020_link_notices :: Query
down_m20261020_link_notices =
[sql|
DROP INDEX idx_group_members_member_restriction_expires_at;
ALTER TABLE group_members DROP COLUMN member_restriction_by_user;
ALTER TABLE group_members DROP COLUMN member_restriction_expires_at;
DROP INDEX idx_link_notices_link_hash;
DROP TABLE link_notices;
|]
+25 -17
View File
@@ -36,7 +36,7 @@ import Data.Type.Equality
import Simplex.Chat.Badges (BadgeRow, MaybeBadgeProofRow, ProofPresHeader, badgeToRow, maybeRowToBadgeProof, rowToBadge)
import Simplex.Chat.Names (SimplexDomainProof, SimplexDomainClaim (..), claimDomain)
import Simplex.Chat.Messages
import Simplex.Chat.Protocol (LinkNotice (..), ReportReason)
import Simplex.Chat.Protocol (LinkNotice (..))
import Simplex.Chat.Remote.Types
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
@@ -702,7 +702,7 @@ type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Text, Maybe
type PublicGroupAccessRow = (Maybe Text, Maybe SimplexDomain, Maybe BoolInt, Maybe BoolInt, Maybe SimplexDomainProof)
type GroupMemberRow = (GroupMemberId, GroupId, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId) :. ProfileRow :. (UTCTime, UTCTime) :. (Maybe UTCTime, Int64, Int64, Int64, Maybe UTCTime, Maybe C.PublicKeyEd25519, Maybe ShortLinkContact, Maybe Text, Maybe UTCTime)
type GroupMemberRow = (GroupMemberId, GroupId, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, BoolInt, Maybe MemberRestrictionStatus, Maybe UTCTime) :.(Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId) :. ProfileRow :. (UTCTime, UTCTime) :. (Maybe UTCTime, Int64, Int64, Int64, Maybe UTCTime, Maybe C.PublicKeyEd25519, Maybe ShortLinkContact, Maybe Text, Maybe UTCTime)
type ProfileRow = (ProfileId, ContactName, Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, Maybe Text, Maybe Text) :. BadgeRow :. ContactDomainRow
@@ -781,10 +781,10 @@ setUserMemberKey db groupId membershipId newKey = do
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_)) =
toGroupMember now userContactId ((groupMemberId, groupId, indexInGroup, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, BI showMessages, memberRestriction_, blockedUntil) :.(invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId) :. profileRow :. (createdAt, updatedAt) :. (supportChatTs_, supportChatUnread, supportChatMemberAttention, supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, relayLink, memberCode_, memberCodeVerifiedAt_)) =
let memberProfile = rowToLocalProfile now profileRow
memberSettings = GroupMemberSettings {showMessages}
blockedByAdmin = maybe False mrsBlocked memberRestriction_
blockedByAdmin = blockedAt now memberRestriction_ blockedUntil
invitedBy = toInvitedBy userContactId invitedById
activeConn = Nothing
memberVerifiedCode = SecurityCode <$> memberCode_ <*> memberCodeVerifiedAt_
@@ -807,7 +807,7 @@ groupMemberQuery :: Query
groupMemberQuery =
[sql|
SELECT
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction,
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.member_restriction_expires_at,
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.preferences_json,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified,
m.created_at, m.updated_at,
@@ -854,7 +854,7 @@ groupInfoQueryFields =
g.root_priv_key, g.root_pub_key, g.member_priv_key,
-- GroupMember - membership
mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
mu.member_status, mu.show_messages, mu.member_restriction, mu.member_restriction_expires_at, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
pu.display_name, pu.full_name, pu.short_descr, pu.description, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, pu.preferences_json,
pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified,
mu.created_at, mu.updated_at,
@@ -1002,30 +1002,38 @@ setContactLinkNotice db ct brokerTs notice =
>>= mapM_ (\linkHash -> upsertLinkNotice db linkHash brokerTs notice)
upsertLinkNotice :: DB.Connection -> ConnReqUriHash -> UTCTime -> LinkNotice -> IO ()
upsertLinkNotice db linkHash brokerTs LinkNotice {ttl, reason} = do
upsertLinkNotice db linkHash brokerTs LinkNotice {ttl} = do
currentTs <- getCurrentTime
let expiresAt = capExpiry $ (\t -> addUTCTime (fromIntegral t) (min brokerTs currentTs)) <$> ttl
DB.execute
db
[sql|
INSERT INTO link_notices (link_hash, expires_at, reason, created_at, updated_at)
VALUES (?,?,?,?,?)
INSERT INTO link_notices (link_hash, expires_at, created_at, updated_at)
VALUES (?,?,?,?)
ON CONFLICT (link_hash) DO UPDATE SET
expires_at = EXCLUDED.expires_at,
reason = EXCLUDED.reason,
updated_at = EXCLUDED.updated_at
|]
(linkHash, expiresAt, reason, currentTs, currentTs)
where
expiresAt = mfilter (< maxLinkNoticeExpiry) $ (\t -> addUTCTime (fromIntegral t) brokerTs) <$> ttl
maxLinkNoticeExpiry = UTCTime (fromGregorian 10000 1 1) 0
(linkHash, expiresAt, currentTs, currentTs)
getLinkNotice :: DB.Connection -> UTCTime -> (ConnReqUriHash, ConnReqUriHash) -> IO (Maybe (Maybe UTCTime, Maybe ReportReason))
capExpiry :: Maybe UTCTime -> Maybe UTCTime
capExpiry = fmap (max minExpiry) . mfilter (< maxExpiry)
where
minExpiry = UTCTime (fromGregorian 1970 1 1) 0
maxExpiry = UTCTime (fromGregorian 10000 1 1) 0
blockExpiry :: MemberRestrictions -> Maybe UTCTime
blockExpiry MemberRestrictions {restriction, expiresAt}
| mrsBlocked restriction = capExpiry expiresAt
| otherwise = Nothing
getLinkNotice :: DB.Connection -> UTCTime -> (ConnReqUriHash, ConnReqUriHash) -> IO (Maybe (Maybe UTCTime))
getLinkNotice db currentTs (linkHash1, linkHash2) =
maybeFirstRow id $
maybeFirstRow fromOnly $
DB.query
db
[sql|
SELECT expires_at, reason
SELECT expires_at
FROM link_notices
WHERE link_hash IN (?,?) AND (expires_at IS NULL OR expires_at > ?)
ORDER BY expires_at IS NULL DESC, expires_at DESC
+8 -3
View File
@@ -1162,14 +1162,18 @@ mrsBlocked = \case
MRSBlocked -> True
_ -> False
blockedAt :: UTCTime -> Maybe MemberRestrictionStatus -> Maybe UTCTime -> Bool
blockedAt now restriction_ blockedUntil = maybe False mrsBlocked restriction_ && maybe True (now <) blockedUntil
data MemberRestrictions = MemberRestrictions
{ restriction :: MemberRestrictionStatus
{ restriction :: MemberRestrictionStatus,
expiresAt :: Maybe UTCTime
}
deriving (Eq, Show)
memberRestrictions :: GroupMember -> Maybe MemberRestrictions
memberRestrictions m
| blockedByAdmin m = Just MemberRestrictions {restriction = MRSBlocked}
| blockedByAdmin m = Just MemberRestrictions {restriction = MRSBlocked, expiresAt = blockedUntil m}
| otherwise = Nothing
data ReceivedGroupInvitation = ReceivedGroupInvitation
@@ -1194,6 +1198,7 @@ data GroupMember = GroupMember
memberStatus :: GroupMemberStatus,
memberSettings :: GroupMemberSettings,
blockedByAdmin :: Bool,
blockedUntil :: Maybe UTCTime,
invitedBy :: InvitedBy,
invitedByGroupMemberId :: Maybe GroupMemberId,
localDisplayName :: ContactName,
@@ -1308,7 +1313,7 @@ data NewGroupMember = NewGroupMember
{ memInfo :: MemberInfo,
memCategory :: GroupMemberCategory,
memStatus :: GroupMemberStatus,
memRestriction :: Maybe MemberRestrictionStatus,
memRestriction :: Maybe MemberRestrictions,
memInvitedBy :: InvitedBy,
memInvitedByGroupMemberId :: Maybe GroupMemberId,
localDisplayName :: ContactName,
+6 -8
View File
@@ -1472,7 +1472,7 @@ viewGroupMembers (Group GroupInfo {membership} members) = map groupMember . filt
GSMemCreator -> ["created group"]
_ -> []
muted m
| blockedByAdmin m = ["blocked by admin"]
| blockedByAdmin m && memberRole' membership >= GRModerator = ["blocked by admin"]
| not (showMessages $ memberSettings m) = ["blocked"]
| otherwise = []
@@ -1892,10 +1892,8 @@ day = T.pack . formatTime defaultTimeLocale "%Y-%m-%d"
dayTime :: UTCTime -> Text
dayTime = T.pack . formatTime defaultTimeLocale "%Y-%m-%d %H:%M"
viewLinkNotice :: Maybe UTCTime -> Maybe ReportReason -> Text
viewLinkNotice expiresAt reason =
maybe "banned permanently" (("banned until " <>) . dayTime) expiresAt
<> maybe "" ((", reason: " <>) . safeDecodeUtf8 . strEncode) reason
viewLinkNotice :: Maybe UTCTime -> Text
viewLinkNotice = maybe "banned permanently" (("banned until " <>) . dayTime)
viewContactInfo :: Contact -> Maybe ConnectionStats -> Maybe Profile -> [StyledString]
viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink, localBadge, contactDomain, contactDomainVerified, description}, activeConn, uiThemes, customData} stats incognitoProfile =
@@ -2284,7 +2282,7 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case
<> contactDomainLine ct
<> ["use " <> ttyToContact' ct <> highlight' "<message>" <> " to send messages"]
CAPContactViaAddress ct -> [ctAddr ("known contact without connection " <> ttyContact' ct)] <> contactDomainLine ct
CAPLinkNotice expiresAt reason -> [ctAddr $ plain $ viewLinkNotice expiresAt reason]
CAPLinkNotice expiresAt _ -> [ctAddr $ plain $ viewLinkNotice expiresAt]
where
ctAddr = ("contact address: " <>)
addrOrBiz = \case
@@ -2319,7 +2317,7 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case
knownGroup prepared = grpOrBizLink g <> ": known " <> prepared <> grpOrBiz g <> " " <> ttyGroup' g
GLPNoRelays _ -> [grpLink "channel has no active relays, please try to join later"]
GLPUpdateRequired _ -> [grpLink "this group requires a newer version of the app, please upgrade"]
GLPLinkNotice expiresAt reason -> [grpLink $ plain $ viewLinkNotice expiresAt reason]
GLPLinkNotice expiresAt -> [grpLink $ plain $ viewLinkNotice expiresAt]
where
connecting g = [grpOrBizLink g <> ": connecting to " <> grpOrBiz g <> " " <> ttyGroup' g]
grpLink = ("group link: " <>)
@@ -2828,7 +2826,7 @@ viewChatError isCmd logLevel testView = \case
<> maybe "" (\MsgMetaJSON {rcvId} -> ", agent msg rcv id: " <> show rcvId) msgMeta_
]
CEConnReqMessageProhibited -> ["message is not allowed with this connection link"]
CELinkNotice expiresAt reason -> ["connection link: " <> plain (viewLinkNotice expiresAt reason)]
CELinkNotice expiresAt -> ["connection link: " <> plain (viewLinkNotice expiresAt)]
CEContactNotFound cName m_ -> viewContactNotFound cName m_
CEContactNotReady c -> [ttyContact' c <> ": not ready"]
CEContactDisabled ct -> [ttyContact' ct <> ": disabled, to enable: " <> highlight ("/enable " <> viewContactName ct) <> ", to delete: " <> highlight ("/d " <> viewContactName ct)]
+37 -1
View File
@@ -68,6 +68,7 @@ chatDirectTests = do
it "delete contact keeping conversation" testDeleteContactKeepConversation
it "delete conversation keeping contact" testDeleteConversationKeepContact
it "delete contact connected via invitation with notice" testDeleteContactInvitationNotice
it "archive contact" testArchiveContact
it "direct message quoted replies" testDirectMessageQuotedReply
it "direct message update" testDirectMessageUpdate
it "direct message edit history" testDirectMessageEditHistory
@@ -616,9 +617,44 @@ testDeleteContactInvitationNotice =
alice <## "bad chat command: notice requires deleting the contact"
alice ##> "/_delete @2 full notice={\"ttl\":86400}"
alice <## "bob: contact is deleted"
bob <## "alice (Alice) deleted contact with you"
bob <// 500000
linkNoticesCount bob `shouldReturn` 0
testArchiveContact :: HasCallStack => TestParams -> IO ()
testArchiveContact =
testChat2 aliceProfile bobProfile $
\alice bob -> do
connectUsers alice bob
alice <##> bob
alice ##> "/_delete @2 archive notice={}"
alice <## "bad chat command: notice requires deleting the contact"
alice ##> "/_delete @2 archive"
alice <## "bob: contact is deleted"
chatDeleted alice `shouldReturn` True
alice #$> ("/_get chat @2 count=1", chat, [(0, "hey")])
bob #> "@alice hi"
alice <# "bob> hi"
chatDeleted alice `shouldReturn` False
alice ##> "/_delete @2 entity notify=off"
alice <## "bob: contact is deleted"
alice ##> "/_delete @2 archive"
alice <## "bob: contact is deleted"
chatDeleted alice `shouldReturn` True
alice #$> ("/_get chat @2 count=1", chat, [(0, "hi")])
alice ##> "/c"
_ <- getInvitation alice
connId <- withCCTransaction alice $ \db -> (\[Only i] -> i :: Int) <$> DB.query_ db "SELECT max(connection_id) FROM connections"
alice ##> ("/_delete :" <> show connId <> " archive")
alice <## "bad chat command: archive is not supported for pending connections"
where
chatDeleted cc =
withCCTransaction cc $ \db ->
(\[Only (DB.BI d)] -> d) <$> DB.query_ db "SELECT chat_deleted FROM contacts WHERE contact_id = 2"
testDeleteConversationKeepContact :: HasCallStack => TestParams -> IO ()
testDeleteConversationKeepContact =
testChat2 aliceProfile bobProfile $
+243 -45
View File
@@ -151,6 +151,10 @@ chatGroupTests = do
it "removal notice applies only to the link used to join" testPlanGroupLinkRemovedNoticeOtherLinks
it "group deletion without notice leaves notices unchanged" testPlanGroupLinkDeletedWithoutNotice
it "rejected pending member with notice can't re-join" testPlanGroupLinkRejectedNotice
it "removed admin with notice sees removal and can't re-join" testPlanGroupLinkRemovedAdminNotice
it "removal with notice of a member on previous version is prohibited" $
runTestCfg2 testCfg testCfgVPrev testRemoveMemberNoticePrevVersion
it "group deleted with kept conversation" testGroupDeleteKeepConversation
#if !defined(dbPostgres)
-- TODO [postgres] restore from outdated db backup (same as in agent)
describe "group message errors" $ do
@@ -248,6 +252,10 @@ chatGroupTests = do
it "block left/removed members" testBlockForAllLeftRemoved
it "mentions of blocked member are ignored" testBlockForAllMentionsIgnored
it "replies of blocked member are not mentions" testBlockedMemberReplyNotMention
it "timed block ends at the end time" testBlockForAllTimed
it "member who set the block sends unblock after the end time" $
runTestCfg3 blockCleanupCfg testCfg testCfgVPrev testBlockForAllExpiryCleanup
it "blocked pending member can't be accepted" testBlockPendingMember
describe "group member inactivity" $ do
it "mark member inactive on reaching quota" testGroupMemberInactive
describe "group member reports" $ do
@@ -3108,27 +3116,32 @@ testPlanGroupLinkRemovedNotice =
gLink <- createTeamLink alice
joinTeamViaLink alice bob gLink "bob"
alice ##> "/rm team bob notice={\"ttl\":86400,\"reason\":\"spam\"}"
removedFromTeam alice bob "bob"
alice ##> "/rm team bob notice={\"ttl\":86400}"
silentlyRemovedFromTeam alice bob "bob" 1
bob ##> ("/_connect plan 1 " <> gLink)
bannedLine bob "group link: " ", reason: spam"
bob ##> ("/_connect plan 1 " <> linkAnotherSchema gLink)
bannedLine bob "group link: " ", reason: spam"
bob <## "group link: known group #team"
bob <## "use #team <message> to send messages"
bob ##> ("/c " <> gLink)
bannedLine bob "group link: " ", reason: spam"
bob ##> ("/_connect 1 " <> gLink)
bannedLine bob "connection link: " ", reason: spam"
bob <## "group link: known group #team"
bob <## "use #team <message> to send messages"
bob #> "#team hello"
alice <// 50000
bob ##> "/d #team"
bob <## "#team: you deleted your local copy of the group"
leaveDeleteTeam bob
bob ##> ("/_connect plan 1 " <> gLink)
bannedLine bob "group link: " ", reason: spam"
bannedLine bob "group link: "
bob ##> ("/_connect plan 1 " <> linkAnotherSchema gLink)
bannedLine bob "group link: "
bob ##> ("/c " <> gLink)
bannedLine bob "group link: "
bob ##> ("/_connect 1 " <> gLink)
bannedLine bob "connection link: "
bob ##> "/create user robert"
showActiveUser bob "robert"
bob ##> ("/_connect plan 2 " <> gLink)
bannedLine bob "group link: " ", reason: spam"
bannedLine bob "group link: "
testPlanGroupLinkRemovedNoticeReplaced :: HasCallStack => TestParams -> IO ()
testPlanGroupLinkRemovedNoticeReplaced =
@@ -3149,20 +3162,22 @@ testPlanGroupLinkRemovedNoticeReplaced =
joinTeamViaLink alice bob gLink "bob_1"
alice ##> "/rm team bob_1 notice={\"ttl\":0}"
removedFromTeam alice bob "bob_1"
silentlyRemovedFromTeam alice bob "bob_1" 1
leaveDeleteTeam bob
bob ##> ("/_connect plan 1 " <> gLink)
bob <## "group link: ok to connect directly"
_sLinkData <- getTermLine bob
linkNoticesCount bob `shouldReturn` 1
bob ##> "/d #team"
bob <## "#team: you deleted your local copy of the group"
joinTeamViaLink alice bob gLink "bob_2"
alice ##> "/rm team bob_2 notice={\"reason\":\"profile\"}"
removedFromTeam alice bob "bob_2"
alice ##> "/rm team bob_2 notice={}"
alice <## "#team: you removed bob_2 from the group (signed)"
permanentLinkNoticesCount bob `shouldEventuallyReturn` 1
bob <// 50000
leaveDeleteTeam bob
bob ##> ("/_connect plan 1 " <> gLink)
bob <## "group link: banned permanently, reason: profile"
bob <## "group link: banned permanently"
linkNoticesCount bob `shouldReturn` 1
testPlanGroupLinkRemovedNoticeOtherLinks :: HasCallStack => TestParams -> IO ()
@@ -3178,9 +3193,10 @@ testPlanGroupLinkRemovedNoticeOtherLinks =
clubLink <- getGroupLink alice "club" GRMember True
alice ##> "/rm team bob notice={\"ttl\":86400}"
removedFromTeam alice bob "bob"
silentlyRemovedFromTeam alice bob "bob" 1
leaveDeleteTeam bob
bob ##> ("/_connect plan 1 " <> gLink)
bannedLine bob "group link: " ""
bannedLine bob "group link: "
bob ##> ("/_connect plan 1 " <> clubLink)
bob <## "group link: ok to connect directly"
_sLinkData <- getTermLine bob
@@ -3225,16 +3241,73 @@ testPlanGroupLinkRejectedNotice =
bob <## "#team: you joined the group, pending approval"
]
alice ##> "/rm team bob notice={\"ttl\":3600,\"reason\":\"community\"}"
alice <## "#team: you removed bob from the group (signed)"
bob <## "#team: alice removed you from the group (signed)"
bob <## "use /d #team to delete the group"
alice ##> "/rm team bob notice={\"ttl\":3600}"
silentlyRemovedFromTeam alice bob "bob" 1
leaveDeleteTeam bob
bob ##> ("/_connect plan 1 " <> gLink)
bannedLine bob "group link: " ", reason: community"
bannedLine bob "group link: "
where
cfg = testCfg {chatHooks = defaultChatHooks {acceptMember = Just (\_ _ _ -> pure $ Right (GAPendingApproval, GRObserver))}}
testPlanGroupLinkRemovedAdminNotice :: HasCallStack => TestParams -> IO ()
testPlanGroupLinkRemovedAdminNotice =
testChat2 aliceProfile bobProfile $
\alice bob -> do
gLink <- createTeamLink alice
joinTeamViaLink alice bob gLink "bob"
alice ##> "/mr team bob admin"
alice <## "#team: you changed the role of bob to admin (signed)"
bob <## "#team: alice changed your role from member to admin (signed)"
alice ##> "/rm team bob notice={\"ttl\":86400}"
removedFromTeam alice bob "bob"
linkNoticesCount bob `shouldReturn` 1
bob ##> "/d #team"
bob <## "#team: you deleted your local copy of the group"
bob ##> ("/_connect plan 1 " <> gLink)
bannedLine bob "group link: "
testRemoveMemberNoticePrevVersion :: HasCallStack => TestCC -> TestCC -> IO ()
testRemoveMemberNoticePrevVersion alice bob = do
createGroup2' "team" alice (bob, GRMember) True
alice ##> "/rm team bob notice={}"
alice <## "bad chat command: notice is not supported for relays, members on old versions and members not in the group"
alice ##> "/rm team bob"
concurrentlyN_
[ alice <## "#team: you removed bob from the group (signed)",
do
bob <## "#team: alice removed you from the group (signed)"
bob <## "use /d #team to delete the group"
]
testGroupDeleteKeepConversation :: HasCallStack => TestParams -> IO ()
testGroupDeleteKeepConversation =
testChat2 aliceProfile bobProfile $
\alice bob -> do
createGroup2' "team" alice (bob, GRMember) True
alice #> "#team hi"
bob <# "#team alice> hi"
bob ##> "/_delete #1 entity"
bob <## "#team: you have insufficient permissions for this action, the required role is owner"
alice ##> "/_delete #1 archive"
alice <## "bad chat command: only full deletion or keeping the conversation is supported for groups"
alice ##> "/_delete #1 messages"
alice <## "bad chat command: only full deletion or keeping the conversation is supported for groups"
alice ##> "/_delete #1 entity"
alice <## "#team: you deleted the group (signed)"
bob <## "#team: alice deleted the group (signed)"
bob <## "use /d #team to delete the local copy of the group"
alice #$> ("/_get chat #1 count=1", chat, [(1, "hi")])
alice ##> "#team hello"
alice <## "bad chat command: not current member"
alice ##> "/_delete #1 entity"
alice <## "bad chat command: keeping the conversation requires an active owner"
alice ##> "/d #team"
alice <## "#team: you deleted your local copy of the group"
createTeamLink :: HasCallStack => TestCC -> IO String
createTeamLink alice = do
threadDelay 100000
@@ -3267,6 +3340,20 @@ removedFromTeam alice bob bobName =
bob <## "use /d #team to delete the group"
]
silentlyRemovedFromTeam :: HasCallStack => TestCC -> TestCC -> String -> Int -> IO ()
silentlyRemovedFromTeam alice bob bobName noticesCount = do
alice <## ("#team: you removed " <> bobName <> " from the group (signed)")
waitLinkNoticesCount bob noticesCount
bob <// 50000
leaveDeleteTeam :: HasCallStack => TestCC -> IO ()
leaveDeleteTeam bob = do
bob ##> "/l team"
bob <## "#team: you left the group"
bob <## "use /d #team to delete the group"
bob ##> "/d #team"
bob <## "#team: you deleted your local copy of the group"
testGroupLinkLargeGroupProfile :: HasCallStack => TestParams -> IO ()
testGroupLinkLargeGroupProfile =
testChatOpts2 testOptsNoFullLinks aliceProfile bobProfile $
@@ -8184,12 +8271,14 @@ testBlockForAllMultipleMembers =
-- bob and cath don't know they are blocked and receive each other's messages
bob #> "#team 3"
[alice, dan] *<# "#team bob> 3 [blocked by admin] <muted>"
alice <# "#team bob> 3 [blocked by admin] <muted>"
cath <# "#team bob> 3"
dan <// 50000
cath #> "#team 4"
[alice, dan] *<# "#team cath> 4 [blocked by admin] <muted>"
alice <# "#team cath> 4 [blocked by admin] <muted>"
bob <# "#team cath> 4"
dan <// 50000
alice ##> "/_block #1 2,3 blocked=off"
alice <## "#team: you unblocked 2 members (signed)"
@@ -8204,6 +8293,112 @@ testBlockForAllMultipleMembers =
cath #> "#team 6"
[alice, bob, dan] *<# "#team cath> 6"
testBlockForAllTimed :: HasCallStack => TestParams -> IO ()
testBlockForAllTimed =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3' "team" alice (bob, GRMember) (cath, GRMember)
threadDelay 1000000
alice #> "#team hi"
[bob, cath] *<# "#team alice> hi"
alice ##> "/block for all #team bob ttl=4"
alice <## "#team: you blocked bob (signed)"
cath <## "#team: alice blocked bob (signed)"
bob <// 50000
alice ##> "/ms team"
alice
<### [ "alice (Alice): owner, you, created group",
"bob (Bob): member, invited, connected, blocked by admin",
"cath (Catherine): member, invited, connected"
]
cath ##> "/ms team"
cath
<### [ "cath (Catherine): member, you, connected",
"alice (Alice): owner, host, connected",
"bob (Bob): member, connected"
]
bob #> "#team 1"
alice <# "#team bob> 1 [blocked by admin] <muted>"
cath <// 50000
alice #$> ("/_get chat #1 count=2", chat, [(1, "blocked bob (signed)"), (0, "1 [blocked by admin]")])
cath #$> ("/_get chat #1 count=1", chat, [(0, "hi")])
threadDelay 4000000
bob #> "#team 2"
[alice, cath] *<# "#team bob> 2"
alice ##> "/ms team"
alice
<### [ "alice (Alice): owner, you, created group",
"bob (Bob): member, invited, connected",
"cath (Catherine): member, invited, connected"
]
testBlockForAllExpiryCleanup :: HasCallStack => TestCC -> TestCC -> TestCC -> IO ()
testBlockForAllExpiryCleanup alice bob cath = do
createGroup3' "team" alice (bob, GRMember) (cath, GRMember)
threadDelay 1000000
alice ##> "/block for all #team cath"
alice <## "#team: you blocked cath (signed)"
bob <## "#team: alice blocked cath (signed)"
cath <// 50000
alice ##> "/block for all #team bob ttl=2"
alice <## "#team: you blocked bob (signed)"
cath <## "#team: alice blocked bob (signed)"
bob <// 50000
cath <## "#team: alice unblocked bob (signed)"
alice #$> ("/_get chat #1 count=3", chat, [(1, "blocked cath (signed)"), (1, "blocked bob (signed)"), (1, "unblocked bob (signed)")])
threadDelay 2000000
alice #$> ("/_get chat #1 count=1", chat, [(1, "unblocked bob (signed)")])
alice ##> "/ms team"
alice
<### [ "alice (Alice): owner, you, created group",
"bob (Bob): member, invited, connected",
"cath (Catherine): member, invited, connected, blocked by admin"
]
bob #> "#team hi"
[alice, cath] *<# "#team bob> hi"
blockCleanupCfg :: ChatConfig
blockCleanupCfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerInterval = 1, cleanupManagerStepDelay = 0}
testBlockPendingMember :: HasCallStack => TestParams -> IO ()
testBlockPendingMember =
testChatCfg2 cfg aliceProfile bobProfile $
\alice bob -> do
gLink <- createTeamLink alice
bob ##> ("/c " <> gLink)
bob <## "connection request sent!"
alice <## "bob (Bob): accepting request to join group #team..."
concurrentlyN_
[ alice <## "#team: bob connected and pending approval, use /_accept member #1 2 <role> to accept member",
do
bob <## "#team: joining the group..."
bob <## "#team: you joined the group, pending approval"
]
alice ##> "/block for all #team bob"
alice <##. "#team: you blocked bob"
bob <// 50000
alice ##> "/_accept member #1 2 member"
alice <## "bad chat command: can't accept blocked member, unblock first"
alice ##> "/unblock for all #team bob"
alice <##. "#team: you unblocked bob"
alice ##> "/_accept member #1 2 member"
concurrentlyN_
[ alice <## "#team: bob accepted",
bob <## "#team: you joined the group"
]
where
cfg = testCfg {chatHooks = defaultChatHooks {acceptMember = Just (\_ _ _ -> pure $ Right (GAPendingApproval, GRObserver))}}
testBlockForAllLeftRemoved :: HasCallStack => TestParams -> IO ()
testBlockForAllLeftRemoved =
testChat4 aliceProfile bobProfile cathProfile danProfile $
@@ -10905,10 +11100,10 @@ testChannelBlockMemberSigned ps =
eve <## "#team: alice blocked cath (signed)"
]
alice #$> ("/_get chat #1 count=1", chat, [(1, "blocked cath (signed)")])
bob #$> ("/_get chat #1 count=1", chat, [(0, "blocked cath (signed)")])
bob #$> ("/_get chat #1 count=1", chat, [(0, "hello from cath")])
cath #$> ("/_get chat #1 count=1", chat, [(1, "hello from cath")]) -- was blocked - no "blocked" chat item
dan #$> ("/_get chat #1 count=1", chat, [(0, "blocked cath (signed)")])
eve #$> ("/_get chat #1 count=1", chat, [(0, "blocked cath (signed)")])
dan #$> ("/_get chat #1 count=1", chat, [(0, "hello from cath")])
eve #$> ("/_get chat #1 count=1", chat, [(0, "hello from cath")])
-- TODO [relays] member: in channels - don't create unknown member record and chat item? (just ignore)
-- block silent member (other members create unknown member record and can verify)
@@ -10925,16 +11120,10 @@ testChannelBlockMemberSigned ps =
eve .<##. ("#team: alice blocked", "(signed)")
]
alice #$> ("/_get chat #1 count=1", chat, [(1, "blocked dan (signed)")])
bob #$> ("/_get chat #1 count=1", chat, [(0, "blocked dan (signed)")])
cath ##> "/_get chat #1 count=1"
[(0, r1)] <- chat <$> getTermLine cath
r1 `shouldStartWith` "blocked"
r1 `shouldEndWith` "(signed)"
dan #$> ("/_get chat #1 count=1", chat, [(0, "blocked cath (signed)")]) -- was blocked - no new chat item
eve ##> "/_get chat #1 count=1"
[(0, r2)] <- chat <$> getTermLine eve
r2 `shouldStartWith` "blocked"
r2 `shouldEndWith` "(signed)"
bob #$> ("/_get chat #1 count=1", chat, [(0, "hello from cath")])
cath #$> ("/_get chat #1 count=1", chat, [(1, "hello from cath")])
dan #$> ("/_get chat #1 count=1", chat, [(0, "hello from cath")])
eve #$> ("/_get chat #1 count=1", chat, [(0, "hello from cath")])
checkMemberRow :: HasCallStack => TestCC -> T.Text -> Maybe T.Text -> IO ()
checkMemberRow cc name expectedRole = memberRoles cc name `shouldReturn` maybeToList expectedRole
@@ -11443,16 +11632,25 @@ testChannelRemoveMemberNotice ps =
memberJoinChannel "team" [bob] [alice] shortLink fullLink cath
threadDelay 1000000
alice ##> "/rm #team cath notice={\"ttl\":86400,\"reason\":\"content\"}"
alice ##> "/rm #team cath notice={\"ttl\":86400}"
alice <## "#team: you removed cath from the group (signed)"
bob <## "#team: alice removed cath from the group (signed)"
cath <## "#team: alice removed you from the group (signed)"
cath <## "use /d #team to delete the group"
waitLinkNoticesCount cath 1
cath <// 50000
cath ##> ("/_connect plan 1 " <> shortLink)
bannedLine cath "group link: " ", reason: content"
cath <## "group link: known group #team"
cath <## "use #team <message> to send messages"
cath ##> "/_connect group #1"
bannedLine cath "connection link: " ", reason: content"
bannedLine cath "connection link: "
cath ##> "/l #team"
cath <## "#team: you left the group"
cath <## "use /d #team to delete the group"
cath ##> "/d #team"
cath <## "#team: you deleted your local copy of the group"
cath ##> ("/_connect plan 1 " <> shortLink)
bannedLine cath "group link: "
-- asserts the member row is GSMemRemoved, with removed_at set (TTL tombstone) or NULL (permanent)
checkRemovedMember :: HasCallStack => TestCC -> String -> Bool -> Expectation
+40 -37
View File
@@ -126,7 +126,6 @@ chatProfileTests = do
it "contact via address" testPlanAddressContactViaAddress
it "contact via short address" testPlanAddressContactViaShortAddress
it "contact deleted silently with notice can't re-connect" testPlanAddressContactDeletedNoticeSilent
it "contact deleted with notification and notice can't re-connect" testPlanAddressContactDeletedNoticeNotify
it "silent notice is not sent to previous version" $
runTestCfg2 testCfg testCfgVPrev testPlanAddressContactDeletedNoticePrevVersion
describe "incognito" $ do
@@ -2160,26 +2159,38 @@ testBusinessAddressNotice = testChat3 businessProfile bobProfile cathProfile $
biz <## "auto_accept on, business"
connectBusinessCustomer biz bob cLink "bob" "Bob"
biz ##> "/rm #bob bob_1 notice={\"ttl\":86400,\"reason\":\"spam\"}"
biz ##> "/rm #bob bob_1 notice={\"ttl\":86400}"
biz <## "#bob: you removed bob_1 from the group (signed)"
bob <## "#biz: biz_1 removed you from the group (signed)"
bob <## "use /d #biz to delete the group"
waitLinkNoticesCount bob 1
bob <// 50000
bob ##> ("/_connect plan 1 " <> cLink)
bannedLine bob "contact address: " ", reason: spam"
bob <## "business address: known business #biz"
bob <## "use #biz <message> to send messages"
leaveDeleteBiz bob
bob ##> ("/_connect plan 1 " <> cLink)
bannedLine bob "contact address: "
bob ##> ("/_connect 1 " <> cLink)
bannedLine bob "connection link: " ", reason: spam"
bannedLine bob "connection link: "
connectBusinessCustomer biz cath cLink "cath" "Catherine"
biz ##> "/_delete #2 full notice={\"reason\":\"other\"}"
biz ##> "/_delete #2 full notice={}"
biz <## "#cath: you deleted the group (signed)"
cath <## "#biz: biz_1 deleted the group (signed)"
cath <## "use /d #biz to delete the local copy of the group"
waitLinkNoticesCount cath 1
cath <// 50000
leaveDeleteBiz cath
cath ##> ("/_connect plan 1 " <> cLink)
cath <## "contact address: banned permanently, reason: other"
cath <## "contact address: banned permanently"
cath ##> ("/c " <> cLink)
cath <## "contact address: banned permanently, reason: other"
cath <## "contact address: banned permanently"
cath ##> ("/_connect 1 " <> cLink)
cath <## "connection link: banned permanently, reason: other"
cath <## "connection link: banned permanently"
where
leaveDeleteBiz cc = do
cc ##> "/l #biz"
cc <## "#biz: you left the group"
cc <## "use /d #biz to delete the group"
cc ##> "/d #biz"
cc <## "#biz: you deleted your local copy of the group"
connectBusinessCustomer :: HasCallStack => TestCC -> TestCC -> String -> String -> String -> IO ()
connectBusinessCustomer biz customer cLink name fullName = do
@@ -2390,45 +2401,37 @@ testPlanAddressContactDeletedReconnected =
testPlanAddressContactDeletedNoticeSilent :: HasCallStack => TestParams -> IO ()
testPlanAddressContactDeletedNoticeSilent =
testChat2 aliceProfile bobProfile $
\alice bob -> do
cLink <- connectViaAddress alice bob
alice ##> "/_delete @2 full notify=off notice={\"ttl\":86400,\"reason\":\"spam\"}"
alice <## "bob: contact is deleted"
waitLinkNoticesCount bob 1
bob ##> ("/_connect plan 1 " <> cLink)
bannedLine bob "contact address: " ", reason: spam"
bob ##> ("/_connect plan 1 " <> linkAnotherSchema cLink)
bannedLine bob "contact address: " ", reason: spam"
bob ##> ("/c " <> cLink)
bannedLine bob "contact address: " ", reason: spam"
bob ##> ("/_connect 1 " <> cLink)
bannedLine bob "connection link: " ", reason: spam"
bob @@@ [("@alice", "hey")]
testPlanAddressContactDeletedNoticeNotify :: HasCallStack => TestParams -> IO ()
testPlanAddressContactDeletedNoticeNotify =
testChat2 aliceProfile bobProfile $
\alice bob -> do
cLink <- connectViaAddress alice bob
alice ##> "/_delete @2 full notice={\"ttl\":86400}"
alice <## "bob: contact is deleted"
bob <## "alice (Alice) deleted contact with you"
waitLinkNoticesCount bob 1
bob <// 50000
bob ##> ("/_connect plan 1 " <> cLink)
bannedLine bob "contact address: " ""
linkNoticesCount bob `shouldReturn` 1
bob <## "contact address: known contact alice"
bob <## "use @alice <message> to send messages"
bob @@@ [("@alice", "hey")]
bob ##> "/d alice"
bob <## "alice: contact is deleted"
bob ##> ("/_connect plan 1 " <> cLink)
bannedLine bob "contact address: "
bob ##> ("/_connect plan 1 " <> linkAnotherSchema cLink)
bannedLine bob "contact address: "
bob ##> ("/c " <> cLink)
bannedLine bob "contact address: "
bob ##> ("/_connect 1 " <> cLink)
bannedLine bob "connection link: "
testPlanAddressContactDeletedNoticePrevVersion :: HasCallStack => TestCC -> TestCC -> IO ()
testPlanAddressContactDeletedNoticePrevVersion alice bob = do
cLink <- connectViaAddress alice bob
alice ##> "/_delete @2 full notify=off notice={\"ttl\":86400}"
alice <## "bob: contact is deleted"
threadDelay 500000
alice <## "bad chat command: contact does not support notice"
bob ##> ("/_connect plan 1 " <> cLink)
bob <## "contact address: known contact alice"
+9 -4
View File
@@ -696,11 +696,16 @@ waitLinkNoticesCount cc n = go (50 :: Int)
then count `shouldBe` n
else threadDelay 100000 >> go (attempts - 1)
bannedLine :: HasCallStack => TestCC -> String -> String -> Expectation
bannedLine cc prefix suffix = do
permanentLinkNoticesCount :: TestCC -> IO Int
permanentLinkNoticesCount cc =
withCCTransaction cc $ \db ->
fromMaybe 0 <$> maybeFirstRow fromOnly (DB.query_ db "SELECT count(1) FROM link_notices WHERE expires_at IS NULL")
bannedLine :: HasCallStack => TestCC -> String -> Expectation
bannedLine cc prefix = do
l <- getTermLine' (Just $ "banned: " <> prefix) cc
let banned = (prefix <> "banned until ") `isPrefixOf` l && suffix `isSuffixOf` l
unless banned $ print ("expected banned line: " <> prefix <> "... " <> suffix, ", got: " <> l)
let banned = (prefix <> "banned until ") `isPrefixOf` l
unless banned $ print ("expected banned line: " <> prefix <> "..., got: " <> l)
banned `shouldBe` True
shouldEventuallyReturn :: (HasCallStack, Eq a, Show a) => IO a -> a -> Expectation
+26 -17
View File
@@ -296,9 +296,6 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
chatInitialVRange
(Just $ SharedMsgId "\1\2\3\4")
(XMsgNew (mcQuote quotedMsg (MCReport "" RRSpam)))
it "x.msg.new report with illegal reason" $
"{\"v\":\"9\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"\",\"reason\":\"illegal\",\"type\":\"report\"}}}"
==# XMsgNew (mcSimple (MCReport "" RRContent))
it "x.msg.new forward with file" $
"{\"v\":\"9\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"},\"forward\":true}}"
##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ (mcForward Nothing (MCText "hello")) {file = Just FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing, fileBadge = Nothing}})
@@ -376,7 +373,10 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
#==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile, memberKey = Nothing} Nothing
it "x.grp.mem.intro with member restrictions" $
"{\"v\":\"9\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberRestrictions\":{\"restriction\":\"blocked\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
#==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile, memberKey = Nothing} (Just MemberRestrictions {restriction = MRSBlocked})
#==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile, memberKey = Nothing} (Just MemberRestrictions {restriction = MRSBlocked, expiresAt = Nothing})
it "x.grp.mem.intro with timed member restrictions" $
"{\"v\":\"9\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberRestrictions\":{\"restriction\":\"blocked\",\"expiresAt\":\"1970-01-01T00:00:01.000000001Z\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
#==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile, memberKey = Nothing} (Just MemberRestrictions {restriction = MRSBlocked, expiresAt = Just $ systemToUTCTime $ MkSystemTime 1 1})
it "x.grp.mem.inv" $
"{\"v\":\"9\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}"
#==# XGrpMemInv (MemberId "\1\2\3\4") IntroInvitation {groupConnReq = testConnReq}
@@ -394,31 +394,40 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
#==# XGrpMemConAll (MemberId "\1\2\3\4")
it "x.grp.mem.del" $
"{\"v\":\"9\",\"event\":\"x.grp.mem.del\",\"params\":{\"memberId\":\"AQIDBA==\"}}"
#==# XGrpMemDel (MemberId "\1\2\3\4") False Nothing Nothing
it "x.grp.mem.del with notice" $
"{\"v\":\"9\",\"event\":\"x.grp.mem.del\",\"params\":{\"memberId\":\"AQIDBA==\",\"notice\":{\"ttl\":86400,\"reason\":\"spam\"}}}"
#==# XGrpMemDel (MemberId "\1\2\3\4") False Nothing (Just LinkNotice {ttl = Just 86400, reason = Just RRSpam})
#==# XGrpMemDel (MemberId "\1\2\3\4") False Nothing False Nothing
it "x.grp.mem.del silent" $
"{\"v\":\"9\",\"event\":\"x.grp.mem.del\",\"params\":{\"memberId\":\"AQIDBA==\",\"silent\":true}}"
#==# XGrpMemDel (MemberId "\1\2\3\4") False Nothing True Nothing
it "x.grp.mem.del silent with notice" $
"{\"v\":\"9\",\"event\":\"x.grp.mem.del\",\"params\":{\"memberId\":\"AQIDBA==\",\"silent\":true,\"notice\":{\"ttl\":86400}}}"
#==# XGrpMemDel (MemberId "\1\2\3\4") False Nothing True (Just LinkNotice {ttl = Just 86400})
it "x.grp.mem.del with permanent notice" $
"{\"v\":\"9\",\"event\":\"x.grp.mem.del\",\"params\":{\"memberId\":\"AQIDBA==\",\"messages\":true,\"silent\":true,\"notice\":{}}}"
#==# XGrpMemDel (MemberId "\1\2\3\4") True Nothing True (Just LinkNotice {ttl = Nothing})
it "x.grp.mem.del with malformed notice" $
"{\"v\":\"9\",\"event\":\"x.grp.mem.del\",\"params\":{\"memberId\":\"AQIDBA==\",\"notice\":{\"ttl\":\"day\"}}}"
==# XGrpMemDel (MemberId "\1\2\3\4") False Nothing Nothing
==# XGrpMemDel (MemberId "\1\2\3\4") False Nothing False Nothing
it "x.grp.mem.restrict" $
"{\"v\":\"9\",\"event\":\"x.grp.mem.restrict\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberRestrictions\":{\"restriction\":\"blocked\"}}}"
#==# XGrpMemRestrict (MemberId "\1\2\3\4") MemberRestrictions {restriction = MRSBlocked, expiresAt = Nothing}
it "x.grp.mem.restrict with end time" $
"{\"v\":\"9\",\"event\":\"x.grp.mem.restrict\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberRestrictions\":{\"restriction\":\"blocked\",\"expiresAt\":\"1970-01-01T00:00:01.000000001Z\"}}}"
#==# XGrpMemRestrict (MemberId "\1\2\3\4") MemberRestrictions {restriction = MRSBlocked, expiresAt = Just $ systemToUTCTime $ MkSystemTime 1 1}
it "x.grp.mem.restrict automatic unblock" $
"{\"v\":\"9\",\"event\":\"x.grp.mem.restrict\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberRestrictions\":{\"restriction\":\"unrestricted\",\"expiresAt\":\"1970-01-01T00:00:01.000000001Z\"}}}"
#==# XGrpMemRestrict (MemberId "\1\2\3\4") MemberRestrictions {restriction = MRSUnrestricted, expiresAt = Just $ systemToUTCTime $ MkSystemTime 1 1}
it "x.grp.leave" $
"{\"v\":\"9\",\"event\":\"x.grp.leave\",\"params\":{}}"
==# XGrpLeave
it "x.grp.del" $
"{\"v\":\"9\",\"event\":\"x.grp.del\",\"params\":{}}"
==# XGrpDel Nothing
it "x.grp.del with notice" $
"{\"v\":\"9\",\"event\":\"x.grp.del\",\"params\":{\"notice\":{\"reason\":\"content\"}}}"
#==# XGrpDel (Just LinkNotice {ttl = Nothing, reason = Just RRContent})
it "x.grp.del with malformed notice" $
"{\"v\":\"9\",\"event\":\"x.grp.del\",\"params\":{\"notice\":1}}"
==# XGrpDel Nothing
==# XGrpDel
it "x.direct.del" $
"{\"v\":\"9\",\"event\":\"x.direct.del\",\"params\":{}}"
#==# XDirectDel False Nothing
it "x.direct.del silent with notice" $
"{\"v\":\"9\",\"event\":\"x.direct.del\",\"params\":{\"silent\":true,\"notice\":{\"ttl\":3600}}}"
#==# XDirectDel True (Just LinkNotice {ttl = Just 3600, reason = Nothing})
#==# XDirectDel True (Just LinkNotice {ttl = Just 3600})
it "x.direct.del with malformed notice" $
"{\"v\":\"9\",\"event\":\"x.direct.del\",\"params\":{\"notice\":\"spam\"}}"
==# XDirectDel False Nothing