mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
core: relax contact probing: don't send probe hashes to new contacts except group link hosts; still send probe hashes to group members (#3180)
This commit is contained in:
+42
-59
@@ -41,9 +41,9 @@ chatDirectTests = do
|
||||
it "direct timed message" testDirectTimedMessage
|
||||
it "repeat AUTH errors disable contact" testRepeatAuthErrorsDisableContact
|
||||
it "should send multiline message" testMultilineMessage
|
||||
describe "contact merge" $ do
|
||||
it "merge duplicate contacts" testContactMerge
|
||||
it "new contact should merge with multiple existing contacts" testMergeContactMultipleContacts
|
||||
describe "duplicate contacts" $ do
|
||||
it "duplicate contacts are separate (contacts don't merge)" testDuplicateContactsSeparate
|
||||
it "new contact is separate with multiple duplicate contacts (contacts don't merge)" testDuplicateContactsMultipleSeparate
|
||||
describe "SMP servers" $ do
|
||||
it "get and set SMP servers" testGetSetSMPServers
|
||||
it "test SMP server connection" testTestSMPServerConnection
|
||||
@@ -170,44 +170,13 @@ testAddContact = versionTestMatrix2 runTestAddContact
|
||||
alice #$> ("/_read chat @2", id, "ok")
|
||||
bob #$> ("/_read chat @2", id, "ok")
|
||||
|
||||
testContactMerge :: HasCallStack => FilePath -> IO ()
|
||||
testContactMerge =
|
||||
testDuplicateContactsSeparate :: HasCallStack => FilePath -> IO ()
|
||||
testDuplicateContactsSeparate =
|
||||
testChat2 aliceProfile bobProfile $
|
||||
\alice bob -> do
|
||||
connectUsers alice bob
|
||||
alice <##> bob
|
||||
|
||||
alice ##> "/c"
|
||||
inv' <- getInvitation alice
|
||||
bob ##> ("/c " <> inv')
|
||||
bob <## "confirmation sent!"
|
||||
concurrentlyN_
|
||||
[ alice
|
||||
<### [ "bob_1 (Bob): contact is connected",
|
||||
"contact bob_1 is merged into bob",
|
||||
"use @bob <message> to send messages"
|
||||
],
|
||||
bob
|
||||
<### [ "alice_1 (Alice): contact is connected",
|
||||
"contact alice_1 is merged into alice",
|
||||
"use @alice <message> to send messages"
|
||||
]
|
||||
]
|
||||
alice <##> bob
|
||||
alice @@@ [("@bob", "hey")]
|
||||
alice `hasContactProfiles` ["alice", "bob"]
|
||||
bob @@@ [("@alice", "hey")]
|
||||
bob `hasContactProfiles` ["bob", "alice"]
|
||||
|
||||
testMergeContactMultipleContacts :: HasCallStack => FilePath -> IO ()
|
||||
testMergeContactMultipleContacts =
|
||||
testChat2 aliceProfile bobProfile $
|
||||
\alice bob -> do
|
||||
bob ##> "/contact_merge off"
|
||||
bob <## "ok"
|
||||
|
||||
connectUsers alice bob
|
||||
|
||||
alice ##> "/c"
|
||||
inv' <- getInvitation alice
|
||||
bob ##> ("/c " <> inv')
|
||||
@@ -216,42 +185,56 @@ testMergeContactMultipleContacts =
|
||||
(alice <## "bob_1 (Bob): contact is connected")
|
||||
(bob <## "alice_1 (Alice): contact is connected")
|
||||
|
||||
alice <##> bob
|
||||
alice #> "@bob_1 1"
|
||||
bob <# "alice_1> 1"
|
||||
bob #> "@alice_1 2"
|
||||
alice <# "bob_1> 2"
|
||||
|
||||
alice @@@ [("@bob", "hey"), ("@bob_1", "2")]
|
||||
alice `hasContactProfiles` ["alice", "bob", "bob"]
|
||||
bob @@@ [("@alice", "hey"), ("@alice_1", "2")]
|
||||
bob `hasContactProfiles` ["bob", "alice", "alice"]
|
||||
|
||||
threadDelay 500000
|
||||
testDuplicateContactsMultipleSeparate :: HasCallStack => FilePath -> IO ()
|
||||
testDuplicateContactsMultipleSeparate =
|
||||
testChat2 aliceProfile bobProfile $
|
||||
\alice bob -> do
|
||||
connectUsers alice bob
|
||||
alice <##> bob
|
||||
|
||||
bob ##> "/contact_merge on"
|
||||
bob <## "ok"
|
||||
alice ##> "/c"
|
||||
inv' <- getInvitation alice
|
||||
bob ##> ("/c " <> inv')
|
||||
bob <## "confirmation sent!"
|
||||
concurrently_
|
||||
(alice <## "bob_1 (Bob): contact is connected")
|
||||
(bob <## "alice_1 (Alice): contact is connected")
|
||||
|
||||
alice ##> "/c"
|
||||
inv'' <- getInvitation alice
|
||||
bob ##> ("/c " <> inv'')
|
||||
bob <## "confirmation sent!"
|
||||
concurrentlyN_
|
||||
[ alice
|
||||
<### [ "bob_2 (Bob): contact is connected",
|
||||
StartsWith "contact bob_2 is merged into bob",
|
||||
StartsWith "use @bob",
|
||||
StartsWith "contact bob_1 is merged into bob",
|
||||
StartsWith "use @bob"
|
||||
],
|
||||
bob
|
||||
<### [ "alice_2 (Alice): contact is connected",
|
||||
StartsWith "contact alice_2 is merged into alice",
|
||||
StartsWith "use @alice",
|
||||
StartsWith "contact alice_1 is merged into alice",
|
||||
StartsWith "use @alice"
|
||||
]
|
||||
]
|
||||
concurrently_
|
||||
(alice <## "bob_2 (Bob): contact is connected")
|
||||
(bob <## "alice_2 (Alice): contact is connected")
|
||||
|
||||
alice <##> bob
|
||||
alice #> "@bob_1 1"
|
||||
bob <# "alice_1> 1"
|
||||
bob #> "@alice_1 2"
|
||||
alice <# "bob_1> 2"
|
||||
alice #> "@bob_2 3"
|
||||
bob <# "alice_2> 3"
|
||||
bob #> "@alice_2 4"
|
||||
alice <# "bob_2> 4"
|
||||
|
||||
alice ##> "/contacts"
|
||||
alice <## "bob (Bob)"
|
||||
alice <### ["bob (Bob)", "bob_1 (Bob)", "bob_2 (Bob)"]
|
||||
bob ##> "/contacts"
|
||||
bob <## "alice (Alice)"
|
||||
alice `hasContactProfiles` ["alice", "bob"]
|
||||
bob `hasContactProfiles` ["bob", "alice"]
|
||||
bob <### ["alice (Alice)", "alice_1 (Alice)", "alice_2 (Alice)"]
|
||||
alice `hasContactProfiles` ["alice", "bob", "bob", "bob"]
|
||||
bob `hasContactProfiles` ["bob", "alice", "alice", "alice"]
|
||||
|
||||
testContactClear :: HasCallStack => FilePath -> IO ()
|
||||
testContactClear =
|
||||
|
||||
+29
-242
@@ -75,12 +75,9 @@ chatGroupTests = do
|
||||
it "members have different local display names in different groups" testNoDirectDifferentLDNs
|
||||
describe "merge members and contacts" $ do
|
||||
it "new member should merge with existing contact" testMergeMemberExistingContact
|
||||
it "new member should merge with multiple existing contacts" testMergeMemberMultipleContacts
|
||||
it "new contact should merge with existing member" testMergeContactExistingMember
|
||||
it "new contact should merge with existing member with associated contact" testMergeContactExistingMemberWithContact
|
||||
it "new contact should merge with multiple existing members" testMergeContactMultipleMembers
|
||||
it "new contact should merge with both existing members and contacts" testMergeContactExistingMembersAndContacts
|
||||
it "new member contact is merged with existing contact" testMergeMemberContact
|
||||
it "new group link host contact should merge with single existing contact out of multiple" testMergeGroupLinkHostMultipleContacts
|
||||
describe "create member contact" $ do
|
||||
it "create contact with group member with invitation message" testMemberContactMessage
|
||||
it "create contact with group member without invitation message" testMemberContactNoMessage
|
||||
@@ -2788,76 +2785,6 @@ testMergeMemberExistingContact =
|
||||
alice `hasContactProfiles` ["alice", "bob", "cath"]
|
||||
cath `hasContactProfiles` ["cath", "alice", "bob"]
|
||||
|
||||
testMergeMemberMultipleContacts :: HasCallStack => FilePath -> IO ()
|
||||
testMergeMemberMultipleContacts =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
alice ##> "/contact_merge off"
|
||||
alice <## "ok"
|
||||
|
||||
connectUsers alice bob
|
||||
connectUsers alice cath
|
||||
|
||||
alice ##> "/c"
|
||||
inv' <- getInvitation alice
|
||||
cath ##> ("/c " <> inv')
|
||||
cath <## "confirmation sent!"
|
||||
concurrently_
|
||||
(alice <## "cath_1 (Catherine): contact is connected")
|
||||
(cath <## "alice_1 (Alice): contact is connected")
|
||||
|
||||
alice `hasContactProfiles` ["alice", "bob", "cath", "cath"]
|
||||
cath `hasContactProfiles` ["cath", "alice", "alice"]
|
||||
|
||||
alice ##> "/contact_merge on"
|
||||
alice <## "ok"
|
||||
|
||||
createGroup2 "team" bob cath
|
||||
bob ##> "/a #team alice"
|
||||
bob <## "invitation to join the group #team sent to alice"
|
||||
alice <## "#team: bob invites you to join the group as member"
|
||||
alice <## "use /j team to accept"
|
||||
alice ##> "/j team"
|
||||
concurrentlyN_
|
||||
[ alice
|
||||
<### [ "#team: you joined the group",
|
||||
"#team: member cath_2 (Catherine) is connected",
|
||||
StartsWith "contact and member are merged: cath",
|
||||
StartsWith "use @cath",
|
||||
StartsWith "contact cath_",
|
||||
StartsWith "use @cath"
|
||||
],
|
||||
bob <## "#team: alice joined the group",
|
||||
cath
|
||||
<### [ "#team: bob added alice_2 (Alice) to the group (connecting...)",
|
||||
"#team: new member alice_2 is connected",
|
||||
StartsWith "contact and member are merged: alice",
|
||||
StartsWith "use @alice",
|
||||
StartsWith "contact alice_",
|
||||
StartsWith "use @alice"
|
||||
]
|
||||
]
|
||||
alice <##> cath
|
||||
alice #> "#team hello"
|
||||
bob <# "#team alice> hello"
|
||||
cath <# "#team alice> hello"
|
||||
cath #> "#team hello too"
|
||||
bob <# "#team cath> hello too"
|
||||
alice <# "#team cath> hello too"
|
||||
|
||||
alice ##> "/contacts"
|
||||
alice
|
||||
<### [ "bob (Bob)",
|
||||
"cath (Catherine)"
|
||||
]
|
||||
cath ##> "/contacts"
|
||||
cath
|
||||
<### [ "alice (Alice)",
|
||||
"bob (Bob)"
|
||||
]
|
||||
alice `hasContactProfiles` ["alice", "bob", "cath"]
|
||||
cath `hasContactProfiles` ["cath", "alice", "bob"]
|
||||
|
||||
testMergeContactExistingMember :: HasCallStack => FilePath -> IO ()
|
||||
testMergeContactExistingMember =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
@@ -2889,66 +2816,6 @@ testMergeContactExistingMember =
|
||||
bob `hasContactProfiles` ["alice", "bob", "cath"]
|
||||
cath `hasContactProfiles` ["cath", "alice", "bob"]
|
||||
|
||||
testMergeContactExistingMemberWithContact :: HasCallStack => FilePath -> IO ()
|
||||
testMergeContactExistingMemberWithContact =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
createGroup3 "team" alice bob cath
|
||||
|
||||
-- create contact, delete only for bob so he would send probe hash to member
|
||||
bob ##> "/_create member contact #1 3"
|
||||
bob <## "contact for member #team cath is created"
|
||||
|
||||
bob ##> "/_invite member contact @3 text hi"
|
||||
bob
|
||||
<### [ "sent invitation to connect directly to member #team cath",
|
||||
WithTime "@cath hi"
|
||||
]
|
||||
cath
|
||||
<### [ "#team bob is creating direct contact bob with you",
|
||||
WithTime "bob> hi"
|
||||
]
|
||||
concurrently_
|
||||
(bob <## "cath (Catherine): contact is connected")
|
||||
(cath <## "bob (Bob): contact is connected")
|
||||
bob <##> cath
|
||||
|
||||
bob ##> "/_delete @3 notify=off"
|
||||
bob <## "cath: contact is deleted"
|
||||
|
||||
bob ##> "/contacts"
|
||||
bob <### ["alice (Alice)"]
|
||||
cath ##> "/contacts"
|
||||
cath <### ["alice (Alice)", "bob (Bob)"]
|
||||
bob `hasContactProfiles` ["alice", "bob", "cath"]
|
||||
cath `hasContactProfiles` ["cath", "alice", "bob"]
|
||||
|
||||
-- contact connects, member is merged
|
||||
bob ##> "/c"
|
||||
inv' <- getInvitation bob
|
||||
cath ##> ("/c " <> inv')
|
||||
cath <## "confirmation sent!"
|
||||
concurrentlyN_
|
||||
[ bob
|
||||
<### [ "cath_1 (Catherine): contact is connected",
|
||||
"contact and member are merged: cath_1, #team cath",
|
||||
"use @cath <message> to send messages"
|
||||
],
|
||||
cath
|
||||
<### [ "bob_1 (Bob): contact is connected",
|
||||
"contact bob_1 is merged into bob",
|
||||
"use @bob <message> to send messages"
|
||||
]
|
||||
]
|
||||
bob <##> cath
|
||||
|
||||
bob ##> "/contacts"
|
||||
bob <### ["alice (Alice)", "cath (Catherine)"]
|
||||
cath ##> "/contacts"
|
||||
cath <### ["alice (Alice)", "bob (Bob)"]
|
||||
bob `hasContactProfiles` ["alice", "bob", "cath"]
|
||||
cath `hasContactProfiles` ["cath", "alice", "bob"]
|
||||
|
||||
testMergeContactMultipleMembers :: HasCallStack => FilePath -> IO ()
|
||||
testMergeContactMultipleMembers =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
@@ -2987,70 +2854,11 @@ testMergeContactMultipleMembers =
|
||||
bob `hasContactProfiles` ["alice", "bob", "cath"]
|
||||
cath `hasContactProfiles` ["cath", "alice", "bob"]
|
||||
|
||||
testMergeContactExistingMembersAndContacts :: HasCallStack => FilePath -> IO ()
|
||||
testMergeContactExistingMembersAndContacts =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
bob ##> "/contact_merge off"
|
||||
bob <## "ok"
|
||||
|
||||
create2Groups3 "team" "club" alice bob cath
|
||||
|
||||
bob ##> "/c"
|
||||
inv' <- getInvitation bob
|
||||
cath ##> ("/c " <> inv')
|
||||
cath <## "confirmation sent!"
|
||||
concurrently_
|
||||
(bob <## "cath_2 (Catherine): contact is connected")
|
||||
(cath <## "bob_2 (Bob): contact is connected")
|
||||
|
||||
bob `hasContactProfiles` ["alice", "bob", "cath", "cath", "cath"]
|
||||
cath `hasContactProfiles` ["cath", "alice", "bob", "bob", "bob"]
|
||||
|
||||
bob ##> "/contact_merge on"
|
||||
bob <## "ok"
|
||||
|
||||
bob ##> "/c"
|
||||
inv'' <- getInvitation bob
|
||||
cath ##> ("/c " <> inv'')
|
||||
cath <## "confirmation sent!"
|
||||
concurrentlyN_
|
||||
[ bob
|
||||
<### [ "cath_3 (Catherine): contact is connected",
|
||||
StartsWith "contact and member are merged: cath",
|
||||
StartsWith "use @cath",
|
||||
StartsWith "contact and member are merged: cath",
|
||||
StartsWith "use @cath",
|
||||
StartsWith "contact cath_3 is merged into cath",
|
||||
StartsWith "use @cath"
|
||||
],
|
||||
cath
|
||||
<### [ "bob_3 (Bob): contact is connected",
|
||||
StartsWith "contact and member are merged: bob",
|
||||
StartsWith "use @bob",
|
||||
StartsWith "contact and member are merged: bob",
|
||||
StartsWith "use @bob",
|
||||
StartsWith "contact bob_3 is merged into bob",
|
||||
StartsWith "use @bob"
|
||||
]
|
||||
]
|
||||
bob <##> cath
|
||||
|
||||
bob ##> "/contacts"
|
||||
bob <### ["alice (Alice)", "cath (Catherine)"]
|
||||
cath ##> "/contacts"
|
||||
cath <### ["alice (Alice)", "bob (Bob)"]
|
||||
bob `hasContactProfiles` ["alice", "bob", "cath"]
|
||||
cath `hasContactProfiles` ["cath", "alice", "bob"]
|
||||
|
||||
testMergeMemberContact :: HasCallStack => FilePath -> IO ()
|
||||
testMergeMemberContact =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
bob ##> "/contact_merge off"
|
||||
bob <## "ok"
|
||||
|
||||
createGroup3 "team" alice bob cath
|
||||
testMergeGroupLinkHostMultipleContacts :: HasCallStack => FilePath -> IO ()
|
||||
testMergeGroupLinkHostMultipleContacts =
|
||||
testChat2 bobProfile cathProfile $
|
||||
\bob cath -> do
|
||||
connectUsers bob cath
|
||||
|
||||
bob ##> "/c"
|
||||
inv' <- getInvitation bob
|
||||
@@ -3060,61 +2868,40 @@ testMergeMemberContact =
|
||||
(bob <## "cath_1 (Catherine): contact is connected")
|
||||
(cath <## "bob_1 (Bob): contact is connected")
|
||||
|
||||
bob `hasContactProfiles` ["alice", "bob", "cath", "cath"]
|
||||
cath `hasContactProfiles` ["cath", "alice", "bob", "bob"]
|
||||
bob `hasContactProfiles` ["bob", "cath", "cath"]
|
||||
cath `hasContactProfiles` ["cath", "bob", "bob"]
|
||||
|
||||
bob ##> "/contact_merge on"
|
||||
bob <## "ok"
|
||||
|
||||
-- bob and cath connect
|
||||
bob ##> "/_create member contact #1 3"
|
||||
bob <## "contact for member #team cath is created"
|
||||
|
||||
bob ##> "/_invite member contact @4 text hi"
|
||||
bob
|
||||
<### [ "sent invitation to connect directly to member #team cath",
|
||||
WithTime "@cath hi"
|
||||
]
|
||||
cath
|
||||
<### [ "#team bob is creating direct contact bob with you",
|
||||
WithTime "bob> hi"
|
||||
]
|
||||
bob ##> "/g party"
|
||||
bob <## "group #party is created"
|
||||
bob <## "to add members use /a party <name> or /create link #party"
|
||||
bob ##> "/create link #party"
|
||||
gLink <- getGroupLink bob "party" GRMember True
|
||||
cath ##> ("/c " <> gLink)
|
||||
cath <## "connection request sent!"
|
||||
bob <## "cath_2 (Catherine): accepting request to join group #party..."
|
||||
concurrentlyN_
|
||||
[ bob
|
||||
<### [ "cath (Catherine): contact is connected",
|
||||
"contact cath_1 is merged into cath",
|
||||
-- StartsWith "use @cath"
|
||||
"use @cath <message> to send messages"
|
||||
<### [ "cath_2 (Catherine): contact is connected",
|
||||
EndsWith "invited to group #party via your group link",
|
||||
EndsWith "joined the group",
|
||||
StartsWith "contact cath_2 is merged into cath",
|
||||
StartsWith "use @cath"
|
||||
],
|
||||
cath
|
||||
<### [ "bob (Bob): contact is connected",
|
||||
"contact bob_1 is merged into bob",
|
||||
-- StartsWith "use @bob"
|
||||
"use @bob <message> to send messages"
|
||||
<### [ "bob_2 (Bob): contact is connected",
|
||||
"#party: you joined the group",
|
||||
StartsWith "contact bob_2 is merged into bob",
|
||||
StartsWith "use @bob"
|
||||
]
|
||||
]
|
||||
bob <##> cath
|
||||
|
||||
bob ##> "/contacts"
|
||||
bob <### ["alice (Alice)", "cath (Catherine)"]
|
||||
bob <### ["cath (Catherine)", "cath_1 (Catherine)"]
|
||||
cath ##> "/contacts"
|
||||
cath <### ["alice (Alice)", "bob (Bob)"]
|
||||
bob `hasContactProfiles` ["alice", "bob", "cath"]
|
||||
cath `hasContactProfiles` ["cath", "alice", "bob"]
|
||||
|
||||
-- group messages work
|
||||
alice #> "#team hello"
|
||||
concurrently_
|
||||
(bob <# "#team alice> hello")
|
||||
(cath <# "#team alice> hello")
|
||||
bob #> "#team hi there"
|
||||
concurrently_
|
||||
(alice <# "#team bob> hi there")
|
||||
(cath <# "#team bob> hi there")
|
||||
cath #> "#team hey team"
|
||||
concurrently_
|
||||
(alice <# "#team cath> hey team")
|
||||
(bob <# "#team cath> hey team")
|
||||
cath <### ["bob (Bob)", "bob_1 (Bob)"]
|
||||
bob `hasContactProfiles` ["bob", "cath", "cath"]
|
||||
cath `hasContactProfiles` ["cath", "bob", "bob"]
|
||||
|
||||
testMemberContactMessage :: HasCallStack => FilePath -> IO ()
|
||||
testMemberContactMessage =
|
||||
|
||||
@@ -20,7 +20,6 @@ chatProfileTests = do
|
||||
it "use multiword profile names" testMultiWordProfileNames
|
||||
describe "user contact link" $ do
|
||||
it "create and connect via contact link" testUserContactLink
|
||||
it "merge existing contact when connecting via contact link" testUserContactLinkMerge
|
||||
it "add contact link to profile" testProfileLink
|
||||
it "auto accept contact requests" testUserContactLinkAutoAccept
|
||||
it "deduplicate contact requests" testDeduplicateContactRequests
|
||||
@@ -219,39 +218,6 @@ testUserContactLink =
|
||||
alice @@@ [("@cath", lastChatFeature), ("@bob", "hey")]
|
||||
alice <##> cath
|
||||
|
||||
testUserContactLinkMerge :: HasCallStack => FilePath -> IO ()
|
||||
testUserContactLinkMerge =
|
||||
testChat2 aliceProfile bobProfile $
|
||||
\alice bob -> do
|
||||
connectUsers alice bob
|
||||
alice <##> bob
|
||||
|
||||
alice ##> "/ad"
|
||||
cLink <- getContactLink alice True
|
||||
bob ##> ("/c " <> cLink)
|
||||
bob <## "connection request sent!"
|
||||
alice <## "bob_1 (Bob) wants to connect to you!"
|
||||
alice <## "to accept: /ac bob_1"
|
||||
alice <## "to reject: /rc bob_1 (the sender will NOT be notified)"
|
||||
alice @@@ [("@bob", "hey"), ("<@bob_1", "")]
|
||||
alice ##> "/ac bob_1"
|
||||
alice <## "bob_1 (Bob): accepting contact request..."
|
||||
concurrentlyN_
|
||||
[ alice
|
||||
<### [ "bob_1 (Bob): contact is connected",
|
||||
"contact bob_1 is merged into bob",
|
||||
"use @bob <message> to send messages"
|
||||
],
|
||||
bob
|
||||
<### [ "alice_1 (Alice): contact is connected",
|
||||
"contact alice_1 is merged into alice",
|
||||
"use @alice <message> to send messages"
|
||||
]
|
||||
]
|
||||
threadDelay 100000
|
||||
alice @@@ [("@bob", lastChatFeature)]
|
||||
alice <##> bob
|
||||
|
||||
testProfileLink :: HasCallStack => FilePath -> IO ()
|
||||
testProfileLink =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
|
||||
Reference in New Issue
Block a user