core, ui: create dummy member record when admin forwards a message from an unknown member (#3651)

* core: create dummy member record when admin forwards a message from an unknown member

* comments

* update unknown member if announced

* change removed

* change unknown name, revert diff

* revert diff

* ios

* update ios library

* android

* remove changes in iOS project file

* rename event

* remove unknown category

* android

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
spaced4ndy 2024-01-11 17:55:13 +04:00 committed by GitHub
parent d9d270f00e
commit bfe5d51df7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 189 additions and 19 deletions

View File

@ -1822,9 +1822,16 @@ public struct GroupMember: Identifiable, Decodable {
public var chatViewName: String { public var chatViewName: String {
get { get {
let p = memberProfile let p = memberProfile
return p.localAlias == "" let name = (
? p.displayName + (p.fullName == "" || p.fullName == p.displayName ? "" : " / \(p.fullName)") p.localAlias == ""
: p.localAlias ? p.displayName + (p.fullName == "" || p.fullName == p.displayName ? "" : " / \(p.fullName)")
: p.localAlias
)
return (
memberStatus == .memUnknown
? String.localizedStringWithFormat(NSLocalizedString("_Previous member_ %@", comment: "previous/unknown group member"), name)
: name
)
} }
} }
@ -1833,6 +1840,7 @@ public struct GroupMember: Identifiable, Decodable {
case .memRemoved: return false case .memRemoved: return false
case .memLeft: return false case .memLeft: return false
case .memGroupDeleted: return false case .memGroupDeleted: return false
case .memUnknown: return false
case .memInvited: return false case .memInvited: return false
case .memIntroduced: return false case .memIntroduced: return false
case .memIntroInvited: return false case .memIntroInvited: return false
@ -1849,6 +1857,7 @@ public struct GroupMember: Identifiable, Decodable {
case .memRemoved: return false case .memRemoved: return false
case .memLeft: return false case .memLeft: return false
case .memGroupDeleted: return false case .memGroupDeleted: return false
case .memUnknown: return false
case .memInvited: return false case .memInvited: return false
case .memIntroduced: return true case .memIntroduced: return true
case .memIntroInvited: return true case .memIntroInvited: return true
@ -1953,6 +1962,7 @@ public enum GroupMemberStatus: String, Decodable {
case memRemoved = "removed" case memRemoved = "removed"
case memLeft = "left" case memLeft = "left"
case memGroupDeleted = "deleted" case memGroupDeleted = "deleted"
case memUnknown = "unknown"
case memInvited = "invited" case memInvited = "invited"
case memIntroduced = "introduced" case memIntroduced = "introduced"
case memIntroInvited = "intro-inv" case memIntroInvited = "intro-inv"
@ -1967,6 +1977,7 @@ public enum GroupMemberStatus: String, Decodable {
case .memRemoved: return "removed" case .memRemoved: return "removed"
case .memLeft: return "left" case .memLeft: return "left"
case .memGroupDeleted: return "group deleted" case .memGroupDeleted: return "group deleted"
case .memUnknown: return "unknown status"
case .memInvited: return "invited" case .memInvited: return "invited"
case .memIntroduced: return "connecting (introduced)" case .memIntroduced: return "connecting (introduced)"
case .memIntroInvited: return "connecting (introduction invitation)" case .memIntroInvited: return "connecting (introduction invitation)"
@ -1983,6 +1994,7 @@ public enum GroupMemberStatus: String, Decodable {
case .memRemoved: return "removed" case .memRemoved: return "removed"
case .memLeft: return "left" case .memLeft: return "left"
case .memGroupDeleted: return "group deleted" case .memGroupDeleted: return "group deleted"
case .memUnknown: return "unknown"
case .memInvited: return "invited" case .memInvited: return "invited"
case .memIntroduced: return "connecting" case .memIntroduced: return "connecting"
case .memIntroInvited: return "connecting" case .memIntroInvited: return "connecting"

View File

@ -1268,12 +1268,19 @@ data class GroupMember (
val verified get() = activeConn?.connectionCode != null val verified get() = activeConn?.connectionCode != null
val chatViewName: String val chatViewName: String
get() = memberProfile.localAlias.ifEmpty { displayName + (if (fullName == "" || fullName == displayName) "" else " / $fullName") } get() {
val name = memberProfile.localAlias.ifEmpty { displayName + (if (fullName == "" || fullName == displayName) "" else " / $fullName") }
return if (memberStatus == GroupMemberStatus.MemUnknown)
String.format(generalGetString(MR.strings.previous_member_vName), name)
else
name
}
val memberActive: Boolean get() = when (this.memberStatus) { val memberActive: Boolean get() = when (this.memberStatus) {
GroupMemberStatus.MemRemoved -> false GroupMemberStatus.MemRemoved -> false
GroupMemberStatus.MemLeft -> false GroupMemberStatus.MemLeft -> false
GroupMemberStatus.MemGroupDeleted -> false GroupMemberStatus.MemGroupDeleted -> false
GroupMemberStatus.MemUnknown -> false
GroupMemberStatus.MemInvited -> false GroupMemberStatus.MemInvited -> false
GroupMemberStatus.MemIntroduced -> false GroupMemberStatus.MemIntroduced -> false
GroupMemberStatus.MemIntroInvited -> false GroupMemberStatus.MemIntroInvited -> false
@ -1288,6 +1295,7 @@ data class GroupMember (
GroupMemberStatus.MemRemoved -> false GroupMemberStatus.MemRemoved -> false
GroupMemberStatus.MemLeft -> false GroupMemberStatus.MemLeft -> false
GroupMemberStatus.MemGroupDeleted -> false GroupMemberStatus.MemGroupDeleted -> false
GroupMemberStatus.MemUnknown -> false
GroupMemberStatus.MemInvited -> false GroupMemberStatus.MemInvited -> false
GroupMemberStatus.MemIntroduced -> true GroupMemberStatus.MemIntroduced -> true
GroupMemberStatus.MemIntroInvited -> true GroupMemberStatus.MemIntroInvited -> true
@ -1377,6 +1385,7 @@ enum class GroupMemberStatus {
@SerialName("removed") MemRemoved, @SerialName("removed") MemRemoved,
@SerialName("left") MemLeft, @SerialName("left") MemLeft,
@SerialName("deleted") MemGroupDeleted, @SerialName("deleted") MemGroupDeleted,
@SerialName("unknown") MemUnknown,
@SerialName("invited") MemInvited, @SerialName("invited") MemInvited,
@SerialName("introduced") MemIntroduced, @SerialName("introduced") MemIntroduced,
@SerialName("intro-inv") MemIntroInvited, @SerialName("intro-inv") MemIntroInvited,
@ -1390,6 +1399,7 @@ enum class GroupMemberStatus {
MemRemoved -> generalGetString(MR.strings.group_member_status_removed) MemRemoved -> generalGetString(MR.strings.group_member_status_removed)
MemLeft -> generalGetString(MR.strings.group_member_status_left) MemLeft -> generalGetString(MR.strings.group_member_status_left)
MemGroupDeleted -> generalGetString(MR.strings.group_member_status_group_deleted) MemGroupDeleted -> generalGetString(MR.strings.group_member_status_group_deleted)
MemUnknown -> generalGetString(MR.strings.group_member_status_unknown)
MemInvited -> generalGetString(MR.strings.group_member_status_invited) MemInvited -> generalGetString(MR.strings.group_member_status_invited)
MemIntroduced -> generalGetString(MR.strings.group_member_status_introduced) MemIntroduced -> generalGetString(MR.strings.group_member_status_introduced)
MemIntroInvited -> generalGetString(MR.strings.group_member_status_intro_invitation) MemIntroInvited -> generalGetString(MR.strings.group_member_status_intro_invitation)
@ -1404,6 +1414,7 @@ enum class GroupMemberStatus {
MemRemoved -> generalGetString(MR.strings.group_member_status_removed) MemRemoved -> generalGetString(MR.strings.group_member_status_removed)
MemLeft -> generalGetString(MR.strings.group_member_status_left) MemLeft -> generalGetString(MR.strings.group_member_status_left)
MemGroupDeleted -> generalGetString(MR.strings.group_member_status_group_deleted) MemGroupDeleted -> generalGetString(MR.strings.group_member_status_group_deleted)
MemUnknown -> generalGetString(MR.strings.group_member_status_unknown_short)
MemInvited -> generalGetString(MR.strings.group_member_status_invited) MemInvited -> generalGetString(MR.strings.group_member_status_invited)
MemIntroduced -> generalGetString(MR.strings.group_member_status_connecting) MemIntroduced -> generalGetString(MR.strings.group_member_status_connecting)
MemIntroInvited -> generalGetString(MR.strings.group_member_status_connecting) MemIntroInvited -> generalGetString(MR.strings.group_member_status_connecting)

View File

@ -1217,6 +1217,7 @@
<string name="group_member_status_removed">removed</string> <string name="group_member_status_removed">removed</string>
<string name="group_member_status_left">left</string> <string name="group_member_status_left">left</string>
<string name="group_member_status_group_deleted">group deleted</string> <string name="group_member_status_group_deleted">group deleted</string>
<string name="group_member_status_unknown">unknown status</string>
<string name="group_member_status_invited">invited</string> <string name="group_member_status_invited">invited</string>
<string name="group_member_status_introduced">connecting (introduced)</string> <string name="group_member_status_introduced">connecting (introduced)</string>
<string name="group_member_status_intro_invitation">connecting (introduction invitation)</string> <string name="group_member_status_intro_invitation">connecting (introduction invitation)</string>
@ -1227,6 +1228,9 @@
<string name="group_member_status_creator">creator</string> <string name="group_member_status_creator">creator</string>
<string name="group_member_status_connecting">connecting</string> <string name="group_member_status_connecting">connecting</string>
<string name="group_member_status_unknown_short">unknown</string>
<string name="previous_member_vName"><![CDATA[<i>Previous member</i> %1$s]]></string>
<!-- AddGroupMembersView.kt --> <!-- AddGroupMembersView.kt -->
<string name="no_contacts_to_add">No contacts to add</string> <string name="no_contacts_to_add">No contacts to add</string>

View File

@ -5100,16 +5100,24 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
_ -> pure conn' _ -> pure conn'
xGrpMemNew :: GroupInfo -> GroupMember -> MemberInfo -> RcvMessage -> UTCTime -> m () xGrpMemNew :: GroupInfo -> GroupMember -> MemberInfo -> RcvMessage -> UTCTime -> m ()
xGrpMemNew gInfo m memInfo@(MemberInfo memId memRole _ memberProfile) msg brokerTs = do xGrpMemNew gInfo m memInfo@(MemberInfo memId memRole _ _) msg brokerTs = do
checkHostRole m memRole checkHostRole m memRole
unless (sameMemberId memId $ membership gInfo) $ unless (sameMemberId memId $ membership gInfo) $
withStore' (\db -> runExceptT $ getGroupMemberByMemberId db user gInfo memId) >>= \case withStore' (\db -> runExceptT $ getGroupMemberByMemberId db user gInfo memId) >>= \case
Right unknownMember@GroupMember {memberStatus = GSMemUnknown} -> do
updatedMember <- withStore $ \db -> updateUnknownMemberAnnounced db user m unknownMember memInfo
toView $ CRUnknownMemberAnnounced user gInfo m unknownMember updatedMember
memberAnnouncedToView updatedMember
Right _ -> messageError "x.grp.mem.new error: member already exists" Right _ -> messageError "x.grp.mem.new error: member already exists"
Left _ -> do Left _ -> do
newMember@GroupMember {groupMemberId} <- withStore $ \db -> createNewGroupMember db user gInfo m memInfo GCPostMember GSMemAnnounced newMember <- withStore $ \db -> createNewGroupMember db user gInfo m memInfo GCPostMember GSMemAnnounced
ci <- saveRcvChatItem user (CDGroupRcv gInfo m) msg brokerTs (CIRcvGroupEvent $ RGEMemberAdded groupMemberId memberProfile) memberAnnouncedToView newMember
groupMsgToView gInfo ci where
toView $ CRJoinedGroupMemberConnecting user gInfo m newMember memberAnnouncedToView announcedMember@GroupMember {groupMemberId, memberProfile} = do
let event = RGEMemberAdded groupMemberId (fromLocalProfile memberProfile)
ci <- saveRcvChatItem user (CDGroupRcv gInfo m) msg brokerTs (CIRcvGroupEvent event)
groupMsgToView gInfo ci
toView $ CRJoinedGroupMemberConnecting user gInfo m announcedMember
xGrpMemIntro :: GroupInfo -> GroupMember -> MemberInfo -> m () xGrpMemIntro :: GroupInfo -> GroupMember -> MemberInfo -> m ()
xGrpMemIntro gInfo@GroupInfo {chatSettings} m@GroupMember {memberRole, localDisplayName = c} memInfo@(MemberInfo memId _ memChatVRange _) = do xGrpMemIntro gInfo@GroupInfo {chatSettings} m@GroupMember {memberRole, localDisplayName = c} memInfo@(MemberInfo memId _ memChatVRange _) = do
@ -5355,8 +5363,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
xGrpMsgForward :: GroupInfo -> GroupMember -> MemberId -> ChatMessage 'Json -> UTCTime -> m () xGrpMsgForward :: GroupInfo -> GroupMember -> MemberId -> ChatMessage 'Json -> UTCTime -> m ()
xGrpMsgForward gInfo@GroupInfo {groupId} m@GroupMember {memberRole, localDisplayName} memberId msg msgTs = do xGrpMsgForward gInfo@GroupInfo {groupId} m@GroupMember {memberRole, localDisplayName} memberId msg msgTs = do
when (memberRole < GRAdmin) $ throwChatError (CEGroupContactRole localDisplayName) when (memberRole < GRAdmin) $ throwChatError (CEGroupContactRole localDisplayName)
author <- withStore $ \db -> getGroupMemberByMemberId db user gInfo memberId withStore' (\db -> runExceptT $ getGroupMemberByMemberId db user gInfo memberId) >>= \case
processForwardedMsg author msg Right author -> processForwardedMsg author msg
Left (SEGroupMemberNotFoundByMemberId _) -> do
let name = T.take 7 . safeDecodeUtf8 . B64.encode . unMemberId $ memberId
unknownAuthor <- withStore $ \db -> createNewUnknownGroupMember db vr user gInfo memberId name
toView $ CRUnknownMemberCreated user gInfo m unknownAuthor
processForwardedMsg unknownAuthor msg
Left e -> throwError $ ChatErrorStore e
where where
-- Note: forwarded group events (see forwardedGroupMsg) should include msgId to be deduplicated -- Note: forwarded group events (see forwardedGroupMsg) should include msgId to be deduplicated
processForwardedMsg :: GroupMember -> ChatMessage 'Json -> m () processForwardedMsg :: GroupMember -> ChatMessage 'Json -> m ()

View File

@ -630,6 +630,8 @@ data ChatResponse
| CRDeletedMember {user :: User, groupInfo :: GroupInfo, byMember :: GroupMember, deletedMember :: GroupMember} | CRDeletedMember {user :: User, groupInfo :: GroupInfo, byMember :: GroupMember, deletedMember :: GroupMember}
| CRDeletedMemberUser {user :: User, groupInfo :: GroupInfo, member :: GroupMember} | CRDeletedMemberUser {user :: User, groupInfo :: GroupInfo, member :: GroupMember}
| CRLeftMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember} | CRLeftMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember}
| CRUnknownMemberCreated {user :: User, groupInfo :: GroupInfo, forwardedByMember :: GroupMember, member :: GroupMember}
| CRUnknownMemberAnnounced {user :: User, groupInfo :: GroupInfo, announcingMember :: GroupMember, unknownMember :: GroupMember, announcedMember :: GroupMember}
| CRGroupEmpty {user :: User, groupInfo :: GroupInfo} | CRGroupEmpty {user :: User, groupInfo :: GroupInfo}
| CRGroupRemoved {user :: User, groupInfo :: GroupInfo} | CRGroupRemoved {user :: User, groupInfo :: GroupInfo}
| CRGroupDeleted {user :: User, groupInfo :: GroupInfo, member :: GroupMember} | CRGroupDeleted {user :: User, groupInfo :: GroupInfo, member :: GroupMember}

View File

@ -110,6 +110,8 @@ module Simplex.Chat.Store.Groups
updateMemberProfile, updateMemberProfile,
getXGrpLinkMemReceived, getXGrpLinkMemReceived,
setXGrpLinkMemReceived, setXGrpLinkMemReceived,
createNewUnknownGroupMember,
updateUnknownMemberAnnounced,
) )
where where
@ -594,11 +596,9 @@ getGroupSummary db User {userId} groupId = do
JOIN group_members m USING (group_id) JOIN group_members m USING (group_id)
WHERE g.user_id = ? WHERE g.user_id = ?
AND g.group_id = ? AND g.group_id = ?
AND m.member_status != ? AND m.member_status NOT IN (?,?,?,?)
AND m.member_status != ?
AND m.member_status != ?
|] |]
(userId, groupId, GSMemRemoved, GSMemLeft, GSMemInvited) (userId, groupId, GSMemRemoved, GSMemLeft, GSMemUnknown, GSMemInvited)
pure GroupSummary {currentMembers = fromMaybe 0 currentMembers_} pure GroupSummary {currentMembers = fromMaybe 0 currentMembers_}
getContactGroupPreferences :: DB.Connection -> User -> Contact -> IO [FullGroupPreferences] getContactGroupPreferences :: DB.Connection -> User -> Contact -> IO [FullGroupPreferences]
@ -682,13 +682,13 @@ getGroupMembersForExpiration db user@User {userId, userContactId} GroupInfo {gro
( groupMemberQuery ( groupMemberQuery
<> [sql| <> [sql|
WHERE m.group_id = ? AND m.user_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) WHERE m.group_id = ? AND m.user_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?)
AND m.member_status IN (?, ?, ?) AND m.member_status IN (?, ?, ?, ?)
AND m.group_member_id NOT IN ( AND m.group_member_id NOT IN (
SELECT DISTINCT group_member_id FROM chat_items SELECT DISTINCT group_member_id FROM chat_items
) )
|] |]
) )
(userId, groupId, userId, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted) (userId, groupId, userId, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted, GSMemUnknown)
toContactMember :: User -> (GroupMemberRow :. MaybeConnectionRow) -> GroupMember toContactMember :: User -> (GroupMemberRow :. MaybeConnectionRow) -> GroupMember
toContactMember User {userContactId} (memberRow :. connRow) = toContactMember User {userContactId} (memberRow :. connRow) =
@ -1339,10 +1339,10 @@ getGroupInfoByGroupLinkHash db vr user@User {userId, userContactId} (groupLinkHa
FROM groups g FROM groups g
JOIN group_members mu ON mu.group_id = g.group_id JOIN group_members mu ON mu.group_id = g.group_id
WHERE g.user_id = ? AND g.via_group_link_uri_hash IN (?,?) WHERE g.user_id = ? AND g.via_group_link_uri_hash IN (?,?)
AND mu.contact_id = ? AND mu.member_status NOT IN (?,?,?) AND mu.contact_id = ? AND mu.member_status NOT IN (?,?,?,?)
LIMIT 1 LIMIT 1
|] |]
(userId, groupLinkHash1, groupLinkHash2, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted) (userId, groupLinkHash1, groupLinkHash2, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted, GSMemUnknown)
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getGroupInfo db vr user) groupId_ maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getGroupInfo db vr user) groupId_
getGroupIdByName :: DB.Connection -> User -> GroupName -> ExceptT StoreError IO GroupId getGroupIdByName :: DB.Connection -> User -> GroupName -> ExceptT StoreError IO GroupId
@ -1965,3 +1965,52 @@ setXGrpLinkMemReceived db mId xGrpLinkMemReceived = do
db db
"UPDATE group_members SET xgrplinkmem_received = ?, updated_at = ? WHERE group_member_id = ?" "UPDATE group_members SET xgrplinkmem_received = ?, updated_at = ? WHERE group_member_id = ?"
(xGrpLinkMemReceived, currentTs, mId) (xGrpLinkMemReceived, currentTs, mId)
createNewUnknownGroupMember :: DB.Connection -> VersionRange -> User -> GroupInfo -> MemberId -> Text -> ExceptT StoreError IO GroupMember
createNewUnknownGroupMember db vr user@User {userId, userContactId} GroupInfo {groupId} memberId memberName = do
currentTs <- liftIO getCurrentTime
let memberProfile = profileFromName memberName
(localDisplayName, profileId) <- createNewMemberProfile_ db user memberProfile currentTs
groupMemberId <- liftIO $ do
DB.execute
db
[sql|
INSERT INTO group_members
( group_id, member_id, member_role, member_category, member_status, invited_by,
user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at,
peer_chat_min_version, peer_chat_max_version)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|]
( (groupId, memberId, GRAuthor, GCPreMember, GSMemUnknown, fromInvitedBy userContactId IBUnknown)
:. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, currentTs, currentTs)
:. (minV, maxV)
)
insertedRowId db
getGroupMemberById db user groupMemberId
where
VersionRange minV maxV = vr
updateUnknownMemberAnnounced :: DB.Connection -> User -> GroupMember -> GroupMember -> MemberInfo -> ExceptT StoreError IO GroupMember
updateUnknownMemberAnnounced db user@User {userId} invitingMember unknownMember@GroupMember {groupMemberId, memberChatVRange} MemberInfo {memberRole, v, profile} = do
_ <- updateMemberProfile db user unknownMember profile
currentTs <- liftIO getCurrentTime
liftIO $
DB.execute
db
[sql|
UPDATE group_members
SET member_role = ?,
member_category = ?,
member_status = ?,
invited_by_group_member_id = ?,
peer_chat_min_version = ?,
peer_chat_max_version = ?,
updated_at = ?
WHERE user_id = ? AND group_member_id = ?
|]
( (memberRole, GCPostMember, GSMemAnnounced, groupMemberId' invitingMember)
:. (minV, maxV, currentTs, userId, groupMemberId)
)
getGroupMemberById db user groupMemberId
where
VersionRange minV maxV = maybe (fromJVersionRange memberChatVRange) fromChatVRange v

View File

@ -825,6 +825,7 @@ data GroupMemberStatus
= GSMemRemoved -- member who was removed from the group = GSMemRemoved -- member who was removed from the group
| GSMemLeft -- member who left the group | GSMemLeft -- member who left the group
| GSMemGroupDeleted -- user member of the deleted group | GSMemGroupDeleted -- user member of the deleted group
| GSMemUnknown -- unknown member, whose message was forwarded by an admin (likely member wasn't introduced due to not being a current member, but message was included in history)
| GSMemInvited -- member is sent to or received invitation to join the group | GSMemInvited -- member is sent to or received invitation to join the group
| GSMemIntroduced -- user received x.grp.mem.intro for this member (only with GCPreMember) | GSMemIntroduced -- user received x.grp.mem.intro for this member (only with GCPreMember)
| GSMemIntroInvited -- member is sent to or received from intro invitation | GSMemIntroInvited -- member is sent to or received from intro invitation
@ -851,6 +852,7 @@ memberActive m = case memberStatus m of
GSMemRemoved -> False GSMemRemoved -> False
GSMemLeft -> False GSMemLeft -> False
GSMemGroupDeleted -> False GSMemGroupDeleted -> False
GSMemUnknown -> False
GSMemInvited -> False GSMemInvited -> False
GSMemIntroduced -> False GSMemIntroduced -> False
GSMemIntroInvited -> False GSMemIntroInvited -> False
@ -869,6 +871,7 @@ memberCurrent' = \case
GSMemRemoved -> False GSMemRemoved -> False
GSMemLeft -> False GSMemLeft -> False
GSMemGroupDeleted -> False GSMemGroupDeleted -> False
GSMemUnknown -> False
GSMemInvited -> False GSMemInvited -> False
GSMemIntroduced -> True GSMemIntroduced -> True
GSMemIntroInvited -> True GSMemIntroInvited -> True
@ -883,6 +886,7 @@ memberRemoved m = case memberStatus m of
GSMemRemoved -> True GSMemRemoved -> True
GSMemLeft -> True GSMemLeft -> True
GSMemGroupDeleted -> True GSMemGroupDeleted -> True
GSMemUnknown -> False
GSMemInvited -> False GSMemInvited -> False
GSMemIntroduced -> False GSMemIntroduced -> False
GSMemIntroInvited -> False GSMemIntroInvited -> False
@ -897,6 +901,7 @@ instance TextEncoding GroupMemberStatus where
"removed" -> Just GSMemRemoved "removed" -> Just GSMemRemoved
"left" -> Just GSMemLeft "left" -> Just GSMemLeft
"deleted" -> Just GSMemGroupDeleted "deleted" -> Just GSMemGroupDeleted
"unknown" -> Just GSMemUnknown
"invited" -> Just GSMemInvited "invited" -> Just GSMemInvited
"introduced" -> Just GSMemIntroduced "introduced" -> Just GSMemIntroduced
"intro-inv" -> Just GSMemIntroInvited "intro-inv" -> Just GSMemIntroInvited
@ -910,6 +915,7 @@ instance TextEncoding GroupMemberStatus where
GSMemRemoved -> "removed" GSMemRemoved -> "removed"
GSMemLeft -> "left" GSMemLeft -> "left"
GSMemGroupDeleted -> "deleted" GSMemGroupDeleted -> "deleted"
GSMemUnknown -> "unknown"
GSMemInvited -> "invited" GSMemInvited -> "invited"
GSMemIntroduced -> "introduced" GSMemIntroduced -> "introduced"
GSMemIntroInvited -> "intro-inv" GSMemIntroInvited -> "intro-inv"

View File

@ -178,6 +178,8 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
CRGroupLinkConnecting u g _ -> ttyUser u [ttyGroup' g <> ": joining the group..."] CRGroupLinkConnecting u g _ -> ttyUser u [ttyGroup' g <> ": joining the group..."]
CRUserDeletedMember u g m -> ttyUser u [ttyGroup' g <> ": you removed " <> ttyMember m <> " from the group"] CRUserDeletedMember u g m -> ttyUser u [ttyGroup' g <> ": you removed " <> ttyMember m <> " from the group"]
CRLeftMemberUser u g -> ttyUser u $ [ttyGroup' g <> ": you left the group"] <> groupPreserved g CRLeftMemberUser u g -> ttyUser u $ [ttyGroup' g <> ": you left the group"] <> groupPreserved g
CRUnknownMemberCreated u g fwdM um -> ttyUser u [ttyGroup' g <> ": " <> ttyMember fwdM <> " forwarded a message from an unknown member, creating unknown member record " <> ttyMember um]
CRUnknownMemberAnnounced u g _ um m -> ttyUser u [ttyGroup' g <> ": unknown member " <> ttyMember um <> " updated to " <> ttyMember m]
CRGroupDeletedUser u g -> ttyUser u [ttyGroup' g <> ": you deleted the group"] CRGroupDeletedUser u g -> ttyUser u [ttyGroup' g <> ": you deleted the group"]
CRRcvFileDescrReady _ _ -> [] CRRcvFileDescrReady _ _ -> []
CRRcvFileDescrNotReady _ _ -> [] CRRcvFileDescrNotReady _ _ -> []
@ -963,6 +965,7 @@ viewGroupMembers (Group GroupInfo {membership} members) = map groupMember . filt
status m = case memberStatus m of status m = case memberStatus m of
GSMemRemoved -> ["removed"] GSMemRemoved -> ["removed"]
GSMemLeft -> ["left"] GSMemLeft -> ["left"]
GSMemUnknown -> ["status unknown"]
GSMemInvited -> ["not yet joined"] GSMemInvited -> ["not yet joined"]
GSMemConnected -> ["connected"] GSMemConnected -> ["connected"]
GSMemComplete -> ["connected"] GSMemComplete -> ["connected"]

View File

@ -132,6 +132,7 @@ chatGroupTests = do
it "deleted message is not included" testGroupHistoryDeletedMessage it "deleted message is not included" testGroupHistoryDeletedMessage
it "disappearing message is sent as disappearing" testGroupHistoryDisappearingMessage it "disappearing message is sent as disappearing" testGroupHistoryDisappearingMessage
it "welcome message (group description) is sent after history" testGroupHistoryWelcomeMessage it "welcome message (group description) is sent after history" testGroupHistoryWelcomeMessage
it "unknown member messages are processed" testGroupHistoryUnknownMember
where where
_0 = supportedChatVRange -- don't create direct connections _0 = supportedChatVRange -- don't create direct connections
_1 = groupCreateDirectVRange _1 = groupCreateDirectVRange
@ -5179,3 +5180,71 @@ testGroupHistoryWelcomeMessage =
[alice, cath] *<# "#team bob> 2" [alice, cath] *<# "#team bob> 2"
cath #> "#team 3" cath #> "#team 3"
[alice, bob] *<# "#team cath> 3" [alice, bob] *<# "#team cath> 3"
testGroupHistoryUnknownMember :: HasCallStack => FilePath -> IO ()
testGroupHistoryUnknownMember =
testChat4 aliceProfile bobProfile cathProfile danProfile $
\alice bob cath dan -> do
createGroup3 "team" alice bob cath
threadDelay 1000000
alice #> "#team hi from alice"
[bob, cath] *<# "#team alice> hi from alice"
threadDelay 1000000
bob #> "#team hi from bob"
[alice, cath] *<# "#team bob> hi from bob"
threadDelay 1000000
cath #> "#team hi from cath"
[alice, bob] *<# "#team cath> hi from cath"
bob ##> "/l team"
concurrentlyN_
[ do
bob <## "#team: you left the group"
bob <## "use /d #team to delete the group",
alice <## "#team: bob left the group",
cath <## "#team: bob left the group"
]
connectUsers alice dan
addMember "team" alice dan GRAdmin
dan ##> "/j team"
concurrentlyN_
[ alice <## "#team: dan joined the group",
dan
<### [ "#team: you joined the group",
WithTime "#team alice> hi from alice [>>]",
StartsWith "#team: alice forwarded a message from an unknown member, creating unknown member record",
EndsWith "hi from bob [>>]",
WithTime "#team cath> hi from cath [>>]",
"#team: member cath (Catherine) is connected"
],
do
cath <## "#team: alice added dan (Daniel) to the group (connecting...)"
cath <## "#team: new member dan is connected"
]
dan ##> "/_get chat #1 count=100"
r <- chat <$> getTermLine dan
r `shouldContain` [(0, "hi from alice"), (0, "hi from bob"), (0, "hi from cath")]
dan ##> "/ms team"
dan
<### [ "dan (Daniel): admin, you, connected",
"alice (Alice): owner, host, connected",
"cath (Catherine): admin, connected",
EndsWith "author, status unknown"
]
-- message delivery works after sending history
alice #> "#team 1"
[cath, dan] *<# "#team alice> 1"
cath #> "#team 2"
[alice, dan] *<# "#team cath> 2"
dan #> "#team 3"
[alice, cath] *<# "#team dan> 3"