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 {
get {
let p = memberProfile
return p.localAlias == ""
let name = (
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 .memLeft: return false
case .memGroupDeleted: return false
case .memUnknown: return false
case .memInvited: return false
case .memIntroduced: return false
case .memIntroInvited: return false
@ -1849,6 +1857,7 @@ public struct GroupMember: Identifiable, Decodable {
case .memRemoved: return false
case .memLeft: return false
case .memGroupDeleted: return false
case .memUnknown: return false
case .memInvited: return false
case .memIntroduced: return true
case .memIntroInvited: return true
@ -1953,6 +1962,7 @@ public enum GroupMemberStatus: String, Decodable {
case memRemoved = "removed"
case memLeft = "left"
case memGroupDeleted = "deleted"
case memUnknown = "unknown"
case memInvited = "invited"
case memIntroduced = "introduced"
case memIntroInvited = "intro-inv"
@ -1967,6 +1977,7 @@ public enum GroupMemberStatus: String, Decodable {
case .memRemoved: return "removed"
case .memLeft: return "left"
case .memGroupDeleted: return "group deleted"
case .memUnknown: return "unknown status"
case .memInvited: return "invited"
case .memIntroduced: return "connecting (introduced)"
case .memIntroInvited: return "connecting (introduction invitation)"
@ -1983,6 +1994,7 @@ public enum GroupMemberStatus: String, Decodable {
case .memRemoved: return "removed"
case .memLeft: return "left"
case .memGroupDeleted: return "group deleted"
case .memUnknown: return "unknown"
case .memInvited: return "invited"
case .memIntroduced: return "connecting"
case .memIntroInvited: return "connecting"

View File

@ -1268,12 +1268,19 @@ data class GroupMember (
val verified get() = activeConn?.connectionCode != null
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) {
GroupMemberStatus.MemRemoved -> false
GroupMemberStatus.MemLeft -> false
GroupMemberStatus.MemGroupDeleted -> false
GroupMemberStatus.MemUnknown -> false
GroupMemberStatus.MemInvited -> false
GroupMemberStatus.MemIntroduced -> false
GroupMemberStatus.MemIntroInvited -> false
@ -1288,6 +1295,7 @@ data class GroupMember (
GroupMemberStatus.MemRemoved -> false
GroupMemberStatus.MemLeft -> false
GroupMemberStatus.MemGroupDeleted -> false
GroupMemberStatus.MemUnknown -> false
GroupMemberStatus.MemInvited -> false
GroupMemberStatus.MemIntroduced -> true
GroupMemberStatus.MemIntroInvited -> true
@ -1377,6 +1385,7 @@ enum class GroupMemberStatus {
@SerialName("removed") MemRemoved,
@SerialName("left") MemLeft,
@SerialName("deleted") MemGroupDeleted,
@SerialName("unknown") MemUnknown,
@SerialName("invited") MemInvited,
@SerialName("introduced") MemIntroduced,
@SerialName("intro-inv") MemIntroInvited,
@ -1390,6 +1399,7 @@ enum class GroupMemberStatus {
MemRemoved -> generalGetString(MR.strings.group_member_status_removed)
MemLeft -> generalGetString(MR.strings.group_member_status_left)
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)
MemIntroduced -> generalGetString(MR.strings.group_member_status_introduced)
MemIntroInvited -> generalGetString(MR.strings.group_member_status_intro_invitation)
@ -1404,6 +1414,7 @@ enum class GroupMemberStatus {
MemRemoved -> generalGetString(MR.strings.group_member_status_removed)
MemLeft -> generalGetString(MR.strings.group_member_status_left)
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)
MemIntroduced -> 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_left">left</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_introduced">connecting (introduced)</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_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 -->
<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'
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
unless (sameMemberId memId $ membership gInfo) $
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"
Left _ -> do
newMember@GroupMember {groupMemberId} <- withStore $ \db -> createNewGroupMember db user gInfo m memInfo GCPostMember GSMemAnnounced
ci <- saveRcvChatItem user (CDGroupRcv gInfo m) msg brokerTs (CIRcvGroupEvent $ RGEMemberAdded groupMemberId memberProfile)
newMember <- withStore $ \db -> createNewGroupMember db user gInfo m memInfo GCPostMember GSMemAnnounced
memberAnnouncedToView newMember
where
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 newMember
toView $ CRJoinedGroupMemberConnecting user gInfo m announcedMember
xGrpMemIntro :: GroupInfo -> GroupMember -> MemberInfo -> m ()
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 gInfo@GroupInfo {groupId} m@GroupMember {memberRole, localDisplayName} memberId msg msgTs = do
when (memberRole < GRAdmin) $ throwChatError (CEGroupContactRole localDisplayName)
author <- withStore $ \db -> getGroupMemberByMemberId db user gInfo memberId
processForwardedMsg author msg
withStore' (\db -> runExceptT $ getGroupMemberByMemberId db user gInfo memberId) >>= \case
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
-- Note: forwarded group events (see forwardedGroupMsg) should include msgId to be deduplicated
processForwardedMsg :: GroupMember -> ChatMessage 'Json -> m ()

View File

@ -630,6 +630,8 @@ data ChatResponse
| CRDeletedMember {user :: User, groupInfo :: GroupInfo, byMember :: GroupMember, deletedMember :: GroupMember}
| CRDeletedMemberUser {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}
| CRGroupRemoved {user :: User, groupInfo :: GroupInfo}
| CRGroupDeleted {user :: User, groupInfo :: GroupInfo, member :: GroupMember}

View File

@ -110,6 +110,8 @@ module Simplex.Chat.Store.Groups
updateMemberProfile,
getXGrpLinkMemReceived,
setXGrpLinkMemReceived,
createNewUnknownGroupMember,
updateUnknownMemberAnnounced,
)
where
@ -594,11 +596,9 @@ getGroupSummary db User {userId} groupId = do
JOIN group_members m USING (group_id)
WHERE g.user_id = ?
AND g.group_id = ?
AND m.member_status != ?
AND m.member_status != ?
AND m.member_status != ?
AND m.member_status NOT IN (?,?,?,?)
|]
(userId, groupId, GSMemRemoved, GSMemLeft, GSMemInvited)
(userId, groupId, GSMemRemoved, GSMemLeft, GSMemUnknown, GSMemInvited)
pure GroupSummary {currentMembers = fromMaybe 0 currentMembers_}
getContactGroupPreferences :: DB.Connection -> User -> Contact -> IO [FullGroupPreferences]
@ -682,13 +682,13 @@ getGroupMembersForExpiration db user@User {userId, userContactId} GroupInfo {gro
( groupMemberQuery
<> [sql|
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 (
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 {userContactId} (memberRow :. connRow) =
@ -1339,10 +1339,10 @@ getGroupInfoByGroupLinkHash db vr user@User {userId, userContactId} (groupLinkHa
FROM groups g
JOIN group_members mu ON mu.group_id = g.group_id
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
|]
(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_
getGroupIdByName :: DB.Connection -> User -> GroupName -> ExceptT StoreError IO GroupId
@ -1965,3 +1965,52 @@ setXGrpLinkMemReceived db mId xGrpLinkMemReceived = do
db
"UPDATE group_members SET xgrplinkmem_received = ?, updated_at = ? WHERE group_member_id = ?"
(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
| GSMemLeft -- member who left the 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
| GSMemIntroduced -- user received x.grp.mem.intro for this member (only with GCPreMember)
| GSMemIntroInvited -- member is sent to or received from intro invitation
@ -851,6 +852,7 @@ memberActive m = case memberStatus m of
GSMemRemoved -> False
GSMemLeft -> False
GSMemGroupDeleted -> False
GSMemUnknown -> False
GSMemInvited -> False
GSMemIntroduced -> False
GSMemIntroInvited -> False
@ -869,6 +871,7 @@ memberCurrent' = \case
GSMemRemoved -> False
GSMemLeft -> False
GSMemGroupDeleted -> False
GSMemUnknown -> False
GSMemInvited -> False
GSMemIntroduced -> True
GSMemIntroInvited -> True
@ -883,6 +886,7 @@ memberRemoved m = case memberStatus m of
GSMemRemoved -> True
GSMemLeft -> True
GSMemGroupDeleted -> True
GSMemUnknown -> False
GSMemInvited -> False
GSMemIntroduced -> False
GSMemIntroInvited -> False
@ -897,6 +901,7 @@ instance TextEncoding GroupMemberStatus where
"removed" -> Just GSMemRemoved
"left" -> Just GSMemLeft
"deleted" -> Just GSMemGroupDeleted
"unknown" -> Just GSMemUnknown
"invited" -> Just GSMemInvited
"introduced" -> Just GSMemIntroduced
"intro-inv" -> Just GSMemIntroInvited
@ -910,6 +915,7 @@ instance TextEncoding GroupMemberStatus where
GSMemRemoved -> "removed"
GSMemLeft -> "left"
GSMemGroupDeleted -> "deleted"
GSMemUnknown -> "unknown"
GSMemInvited -> "invited"
GSMemIntroduced -> "introduced"
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..."]
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
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"]
CRRcvFileDescrReady _ _ -> []
CRRcvFileDescrNotReady _ _ -> []
@ -963,6 +965,7 @@ viewGroupMembers (Group GroupInfo {membership} members) = map groupMember . filt
status m = case memberStatus m of
GSMemRemoved -> ["removed"]
GSMemLeft -> ["left"]
GSMemUnknown -> ["status unknown"]
GSMemInvited -> ["not yet joined"]
GSMemConnected -> ["connected"]
GSMemComplete -> ["connected"]

View File

@ -132,6 +132,7 @@ chatGroupTests = do
it "deleted message is not included" testGroupHistoryDeletedMessage
it "disappearing message is sent as disappearing" testGroupHistoryDisappearingMessage
it "welcome message (group description) is sent after history" testGroupHistoryWelcomeMessage
it "unknown member messages are processed" testGroupHistoryUnknownMember
where
_0 = supportedChatVRange -- don't create direct connections
_1 = groupCreateDirectVRange
@ -5179,3 +5180,71 @@ testGroupHistoryWelcomeMessage =
[alice, cath] *<# "#team bob> 2"
cath #> "#team 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"