mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-10 16:31:48 +00:00
implement, test
This commit is contained in:
@@ -114,7 +114,7 @@ groupDirectoryEntry now GroupInfo {groupProfile, chatTs, createdAt, groupSummary
|
||||
fromMaybe (img, ".jpg") $
|
||||
(,".jpg") <$> T.stripPrefix "data:image/jpg;base64," img
|
||||
<|> (,".png") <$> T.stripPrefix "data:image/png;base64," img
|
||||
imgName = B.unpack $ B64URL.encodeUnpadded $ BA.convert $ (CH.hash :: ByteString -> Digest MD5) $ strEncode (connFullLink groupLink)
|
||||
imgName = B.unpack $ B64URL.encodeUnpadded $ BA.convert $ (CH.hash :: ByteString -> Digest MD5) $ strEncode (let CCLink fl _ = groupLink in fl)
|
||||
imgFile = listingImageFolder </> imgName <> imgExt
|
||||
in case B64.decode $ encodeUtf8 img' of
|
||||
Right img'' -> Just (imgFile, img'')
|
||||
|
||||
@@ -282,6 +282,7 @@ library
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20260122_has_link
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20260222_chat_relays
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20260403_item_viewed
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20260413_chat_hidden
|
||||
other-modules:
|
||||
Paths_simplex_chat
|
||||
hs-source-dirs:
|
||||
|
||||
@@ -376,6 +376,7 @@ data ChatCommand
|
||||
| APIGetNtfConns {nonce :: C.CbNonce, encNtfInfo :: ByteString}
|
||||
| APIGetConnNtfMessages (NonEmpty ConnMsgReq)
|
||||
| APIAddMember {groupId :: GroupId, contactId :: ContactId, memberRole :: GroupMemberRole}
|
||||
| APISharePublicGroup {groupId :: GroupId, toChatRef :: ChatRef}
|
||||
| APIJoinGroup {groupId :: GroupId, enableNtfs :: MsgFilter}
|
||||
| APIAcceptMember {groupId :: GroupId, groupMemberId :: GroupMemberId, memberRole :: GroupMemberRole}
|
||||
| APIDeleteMemberSupportChat GroupId GroupMemberId
|
||||
@@ -518,6 +519,7 @@ data ChatCommand
|
||||
| APIGetGroupRelays {groupId :: GroupId}
|
||||
| NewPublicGroup IncognitoEnabled (NonEmpty Int64) GroupProfile
|
||||
| AddMember GroupName ContactName GroupMemberRole
|
||||
| SharePublicGroup GroupName ContactName
|
||||
| JoinGroup {groupName :: GroupName, enableNtfs :: MsgFilter}
|
||||
| AcceptMember GroupName ContactName GroupMemberRole
|
||||
| MemberRole GroupName ContactName GroupMemberRole
|
||||
@@ -732,6 +734,7 @@ data ChatResponse
|
||||
| CRUserDeletedMembers {user :: User, groupInfo :: GroupInfo, members :: [GroupMember], withMessages :: Bool, msgSigned :: Bool}
|
||||
| CRGroupsList {user :: User, groups :: [GroupInfo]}
|
||||
| CRSentGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, member :: GroupMember}
|
||||
| CRSentPublicGroupInvitation {user :: User, groupInfo :: GroupInfo}
|
||||
| CRFileTransferStatus User (FileTransfer, [Integer]) -- TODO refactor this type to FileTransferStatus
|
||||
| CRFileTransferStatusXFTP User AChatItem
|
||||
| CRUserProfile {user :: User, profile :: Profile}
|
||||
|
||||
@@ -101,6 +101,7 @@ import qualified Simplex.Messaging.Crypto.ShortLink as SL
|
||||
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
|
||||
import qualified Simplex.Messaging.Crypto.File as CF
|
||||
import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), pattern IKPQOff, pattern IKPQOn, pattern PQEncOff, pattern PQSupportOff, pattern PQSupportOn)
|
||||
import Simplex.Messaging.Encoding (smpEncode)
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (base64P)
|
||||
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), MsgFlags (..), NtfServer, ProtoServerWithAuth (..), ProtocolServer, ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol)
|
||||
@@ -2470,6 +2471,37 @@ processChatCommand vr nm = \case
|
||||
relays <- liftIO $ getGroupRelays db gInfo
|
||||
pure (gInfo, relays)
|
||||
pure $ CRGroupRelays user gInfo relays
|
||||
APISharePublicGroup groupId toChatRef -> withUser $ \user -> do
|
||||
gInfo@GroupInfo {groupProfile, membership, groupSummary, localDisplayName = gName} <- withFastStore $ \db -> getGroupInfo db vr user groupId
|
||||
unless (useRelays' gInfo) $ throwCmdError "not a public group"
|
||||
let groupSize = fromIntegral $ currentMembers groupSummary
|
||||
GroupMember {memberId = mId, groupMemberId = mGroupMemberId} = membership
|
||||
ciGroupInv oss = CIGroupInvitation {groupId, groupMemberId = mGroupMemberId, localDisplayName = gName, groupProfile, status = CIGISPending, ownerSigStatus = oss}
|
||||
case toChatRef of
|
||||
ChatRef CTDirect contactId _ -> do
|
||||
ct@Contact {activeConn = Just ctConn} <- withFastStore $ \db -> getContact db vr user contactId
|
||||
groupOwner <- forM (groupKeys gInfo) $ \GroupKeys {memberPrivKey} -> do
|
||||
adHash <- withAgent $ \a -> getConnectionRatchetAdHash a (aConnId ctConn)
|
||||
let invBody = encodeUtf8 $ encodeJSON groupProfile
|
||||
sig = C.sign' memberPrivKey $ smpEncode CBDirect <> adHash <> invBody
|
||||
pure GroupOwnerSig {memberId = mId, binding = B64UrlByteString adHash, ownerSig = B64UrlByteString $ C.signatureBytes sig}
|
||||
let inv = PublicGroupInvitation {groupProfile, groupOwner, groupSize}
|
||||
oss = if isJust groupOwner then Just OSSVerified else Nothing
|
||||
(msg, _) <- sendDirectContactMessage user ct $ XGrpInvPub inv
|
||||
ci <- saveSndChatItem' user (CDDirectSnd ct) msg (CISndGroupInvitation (ciGroupInv oss) GRObserver) Nothing Nothing Nothing Nothing False
|
||||
toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci]
|
||||
pure $ CRSentPublicGroupInvitation user gInfo
|
||||
ChatRef CTGroup toGroupId scope -> do
|
||||
(toGInfo, members) <- withFastStore $ \db -> do
|
||||
g <- getGroupInfo db vr user toGroupId
|
||||
ms <- liftIO $ getGroupMembers db vr user g
|
||||
pure (g, ms)
|
||||
let inv = PublicGroupInvitation {groupProfile, groupOwner = Nothing, groupSize}
|
||||
msg <- sendGroupMessage user toGInfo scope members $ XGrpInvPub inv
|
||||
ci <- saveSndChatItem' user (CDGroupSnd toGInfo Nothing) msg (CISndGroupInvitation (ciGroupInv Nothing) GRObserver) Nothing Nothing Nothing Nothing False
|
||||
toView $ CEvtNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat toGInfo Nothing) ci]
|
||||
pure $ CRSentPublicGroupInvitation user gInfo
|
||||
_ -> throwCmdError "unsupported chat type"
|
||||
APIAddMember groupId contactId memRole -> withUser $ \user -> withGroupLock "addMember" groupId $ do
|
||||
-- TODO for large groups: no need to load all members to determine if contact is a member
|
||||
(group, contact) <- withFastStore $ \db -> (,) <$> getGroup db vr user groupId <*> getContact db vr user contactId
|
||||
@@ -2857,6 +2889,9 @@ processChatCommand vr nm = \case
|
||||
AddMember gName cName memRole -> withUser $ \user -> do
|
||||
(groupId, contactId) <- withFastStore $ \db -> (,) <$> getGroupIdByName db user gName <*> getContactIdByName db user cName
|
||||
processChatCommand vr nm $ APIAddMember groupId contactId memRole
|
||||
SharePublicGroup gName cName -> withUser $ \user -> do
|
||||
(groupId, contactId) <- withFastStore $ \db -> (,) <$> getGroupIdByName db user gName <*> getContactIdByName db user cName
|
||||
processChatCommand vr nm $ APISharePublicGroup groupId (ChatRef CTDirect contactId Nothing)
|
||||
JoinGroup gName enableNtfs -> withUser $ \user -> do
|
||||
groupId <- withFastStore $ \db -> getGroupIdByName db user gName
|
||||
processChatCommand vr nm $ APIJoinGroup groupId enableNtfs
|
||||
@@ -4809,6 +4844,8 @@ chatCommandP =
|
||||
"/_ntf conns " *> (APIGetNtfConns <$> strP <* A.space <*> strP),
|
||||
"/_ntf conn messages " *> (APIGetConnNtfMessages <$> connMsgsP),
|
||||
"/_add #" *> (APIAddMember <$> A.decimal <* A.space <*> A.decimal <*> memberRole),
|
||||
"/_share #" *> (APISharePublicGroup <$> A.decimal <* A.space <*> chatRefP),
|
||||
"/share " *> char_ '#' *> (SharePublicGroup <$> displayNameP <* A.space <* char_ '@' <*> displayNameP),
|
||||
"/_join #" *> (APIJoinGroup <$> A.decimal <*> pure MFAll), -- needs to be changed to support in UI
|
||||
"/_accept member #" *> (APIAcceptMember <$> A.decimal <* A.space <*> A.decimal <*> memberRole),
|
||||
"/_delete member chat #" *> (APIDeleteMemberSupportChat <$> A.decimal <* A.space <*> A.decimal),
|
||||
|
||||
@@ -519,6 +519,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
XInfo p -> xInfo ct'' p
|
||||
XDirectDel -> xDirectDel ct'' msg msgMeta
|
||||
XGrpInv gInv -> processGroupInvitation ct'' gInv msg msgMeta
|
||||
XGrpInvPub pubGrpInv -> processPublicGroupInvitation ct'' conn' pubGrpInv msg msgMeta
|
||||
XInfoProbe probe -> xInfoProbe (COMContact ct'') probe
|
||||
XInfoProbeCheck probeHash -> xInfoProbeCheck (COMContact ct'') probeHash
|
||||
XInfoProbeOk probe -> xInfoProbeOk (COMContact ct'') probe
|
||||
@@ -675,6 +676,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
forM_ (autoReply $ addressSettings ucl) $ \mc -> do
|
||||
connReq_ <- pure (contactRequestId' ct) $>>= \connReqId -> withStore' (\db -> getContactRequest' db user connReqId)
|
||||
sendAutoReply ct mc connReq_
|
||||
LDATA fixedLinkData cData ->
|
||||
withCompletedCommand conn agentMsg $ \CommandData {cmdFunction} ->
|
||||
case cmdFunction of
|
||||
CFGetGroupDataInv -> processPublicGroupLinkData user ct conn fixedLinkData cData
|
||||
_ -> throwChatError $ CECommandError "unexpected cmdFunction"
|
||||
QCONT ->
|
||||
void $ continueSending connEntity conn
|
||||
MWARN msgId err -> do
|
||||
@@ -2408,6 +2414,37 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
sameGroupLinkId (Just gli) (Just gli') = gli == gli'
|
||||
sameGroupLinkId _ _ = False
|
||||
|
||||
processPublicGroupInvitation :: Contact -> Connection -> PublicGroupInvitation -> RcvMessage -> MsgMeta -> CM ()
|
||||
processPublicGroupInvitation ct conn PublicGroupInvitation {groupProfile, groupOwner, groupSize} msg msgMeta = do
|
||||
unless (isJust $ publicGroup groupProfile) $ messageError "x.grp.inv.pub: not a public group"
|
||||
let oss = case groupOwner of
|
||||
Nothing -> Nothing
|
||||
Just _ -> Just OSSPending
|
||||
sLnk_ = (\PublicGroupProfile {groupLink = gl} -> gl) <$> publicGroup groupProfile
|
||||
prepLink = PreparedConnLink {connFullLink = Nothing, connShortLink = sLnk_}
|
||||
gVar <- asks random
|
||||
subRole <- asks $ channelSubscriberRole . config
|
||||
(gInfo@GroupInfo {groupId, localDisplayName = gName, membership}, _) <- withStore $ \db ->
|
||||
createPreparedGroup db gVar vr user groupProfile False prepLink Nothing True subRole (Just $ fromIntegral groupSize)
|
||||
withStore' $ \db -> setGroupChatHidden db user groupId True
|
||||
let GroupMember {groupMemberId} = membership
|
||||
content = CIRcvGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName = gName, groupProfile, status = CIGISPending, ownerSigStatus = oss}) GRObserver
|
||||
(ci, cInfo) <- saveRcvChatItemNoParse user (CDDirectRcv ct) msg brokerTs content
|
||||
withStore' $ \db -> setGroupInvitationChatItemId db user groupId (chatItemId' ci)
|
||||
toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv cInfo ci]
|
||||
toView $ CEvtReceivedGroupInvitation {user, groupInfo = gInfo, contact = ct, fromMemberRole = GROwner, memberRole = GRObserver}
|
||||
-- start async link resolution for signature verification
|
||||
forM_ groupOwner $ \_ ->
|
||||
forM_ sLnk_ $ \sLnk ->
|
||||
void $ getAgentConnShortLinkAsync user CFGetGroupDataInv (Just conn) sLnk
|
||||
where
|
||||
brokerTs = metaBrokerTs msgMeta
|
||||
|
||||
processPublicGroupLinkData :: User -> Contact -> Connection -> FixedLinkData 'CMContact -> ConnLinkData 'CMContact -> CM ()
|
||||
processPublicGroupLinkData _user _ct _conn _fixedLinkData _cData = do
|
||||
-- TODO: verify owner signature, update chat item ownerSigStatus
|
||||
pure ()
|
||||
|
||||
checkIntegrityCreateItem :: forall c. ChatTypeI c => ChatDirection c 'MDRcv -> MsgMeta -> CM ()
|
||||
checkIntegrityCreateItem cd MsgMeta {integrity, broker = (_, brokerTs)} = case integrity of
|
||||
MsgOk -> pure ()
|
||||
|
||||
@@ -40,6 +40,7 @@ module Simplex.Chat.Store.Groups
|
||||
createGroupInvitedViaLink,
|
||||
createGroupRejectedViaLink,
|
||||
setGroupInvitationChatItemId,
|
||||
setGroupChatHidden,
|
||||
getGroup,
|
||||
getGroupInfoByUserContactLinkConnReq,
|
||||
getGroupInfoViaUserShortLink,
|
||||
@@ -879,6 +880,11 @@ setGroupInvitationChatItemId db User {userId} groupId chatItemId = do
|
||||
currentTs <- getCurrentTime
|
||||
DB.execute db "UPDATE groups SET chat_item_id = ?, updated_at = ? WHERE user_id = ? AND group_id = ?" (chatItemId, currentTs, userId, groupId)
|
||||
|
||||
setGroupChatHidden :: DB.Connection -> User -> GroupId -> Bool -> IO ()
|
||||
setGroupChatHidden db User {userId} groupId hidden = do
|
||||
currentTs <- getCurrentTime
|
||||
DB.execute db "UPDATE groups SET chat_hidden = ?, updated_at = ? WHERE user_id = ? AND group_id = ?" (BI hidden, currentTs, userId, groupId)
|
||||
|
||||
-- TODO return the last connection that is ready, not any last connection
|
||||
-- requires updating connection status
|
||||
getGroup :: DB.Connection -> VersionRangeChat -> User -> GroupId -> ExceptT StoreError IO Group
|
||||
|
||||
@@ -915,7 +915,7 @@ findGroupChatPreviews_ db User {userId} pagination clq =
|
||||
baseParams = (userId, userId, CISRcvNew, userId, MCReport_, BI False)
|
||||
getPreviews = case clq of
|
||||
CLQFilters {favorite = False, unread = False} -> do
|
||||
let q = baseQuery <> " WHERE g.user_id = ? AND g.creating_in_progress = 0"
|
||||
let q = baseQuery <> " WHERE g.user_id = ? AND g.creating_in_progress = 0 AND g.chat_hidden = 0"
|
||||
p = baseParams :. Only userId
|
||||
queryWithPagination q p
|
||||
CLQFilters {favorite = True, unread = False} -> do
|
||||
@@ -923,7 +923,7 @@ findGroupChatPreviews_ db User {userId} pagination clq =
|
||||
baseQuery
|
||||
<> " "
|
||||
<> [sql|
|
||||
WHERE g.user_id = ? AND g.creating_in_progress = 0
|
||||
WHERE g.user_id = ? AND g.creating_in_progress = 0 AND g.chat_hidden = 0
|
||||
AND g.favorite = 1
|
||||
|]
|
||||
p = baseParams :. Only userId
|
||||
@@ -933,7 +933,7 @@ findGroupChatPreviews_ db User {userId} pagination clq =
|
||||
baseQuery
|
||||
<> " "
|
||||
<> [sql|
|
||||
WHERE g.user_id = ? AND g.creating_in_progress = 0
|
||||
WHERE g.user_id = ? AND g.creating_in_progress = 0 AND g.chat_hidden = 0
|
||||
AND (g.unread_chat = 1 OR ChatStats.UnreadCount > 0)
|
||||
|]
|
||||
p = baseParams :. Only userId
|
||||
@@ -943,7 +943,7 @@ findGroupChatPreviews_ db User {userId} pagination clq =
|
||||
baseQuery
|
||||
<> " "
|
||||
<> [sql|
|
||||
WHERE g.user_id = ? AND g.creating_in_progress = 0
|
||||
WHERE g.user_id = ? AND g.creating_in_progress = 0 AND g.chat_hidden = 0
|
||||
AND (g.favorite = 1
|
||||
OR g.unread_chat = 1 OR ChatStats.UnreadCount > 0)
|
||||
|]
|
||||
@@ -955,7 +955,7 @@ findGroupChatPreviews_ db User {userId} pagination clq =
|
||||
<> " "
|
||||
<> [sql|
|
||||
JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id
|
||||
WHERE g.user_id = ? AND g.creating_in_progress = 0
|
||||
WHERE g.user_id = ? AND g.creating_in_progress = 0 AND g.chat_hidden = 0
|
||||
AND (
|
||||
LOWER(g.local_display_name) LIKE '%' || ? || '%'
|
||||
OR LOWER(gp.display_name) LIKE '%' || ? || '%'
|
||||
|
||||
@@ -151,6 +151,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20260108_chat_indices
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260122_has_link
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260222_chat_relays
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260403_item_viewed
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260413_chat_hidden
|
||||
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
|
||||
|
||||
schemaMigrations :: [(String, Query, Maybe Query)]
|
||||
@@ -301,7 +302,8 @@ schemaMigrations =
|
||||
("20260108_chat_indices", m20260108_chat_indices, Just down_m20260108_chat_indices),
|
||||
("20260122_has_link", m20260122_has_link, Just down_m20260122_has_link),
|
||||
("20260222_chat_relays", m20260222_chat_relays, Just down_m20260222_chat_relays),
|
||||
("20260403_item_viewed", m20260403_item_viewed, Just down_m20260403_item_viewed)
|
||||
("20260403_item_viewed", m20260403_item_viewed, Just down_m20260403_item_viewed),
|
||||
("20260413_chat_hidden", m20260413_chat_hidden, Just down_m20260413_chat_hidden)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Store.SQLite.Migrations.M20260413_chat_hidden where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20260413_chat_hidden :: Query
|
||||
m20260413_chat_hidden =
|
||||
[sql|
|
||||
ALTER TABLE groups ADD COLUMN chat_hidden INTEGER NOT NULL DEFAULT 0;
|
||||
|]
|
||||
|
||||
down_m20260413_chat_hidden :: Query
|
||||
down_m20260413_chat_hidden =
|
||||
[sql|
|
||||
ALTER TABLE groups DROP COLUMN chat_hidden;
|
||||
|]
|
||||
@@ -922,7 +922,8 @@ data GroupOwnerSig = GroupOwnerSig
|
||||
-- | Context binding: ratchetAdHash (direct chat) or smpEncode (publicGroupId, memberId) (public group).
|
||||
-- Verifier computes expected value from context and compares — mismatch indicates MitM or wrong context.
|
||||
binding :: B64UrlByteString,
|
||||
ownerSig :: C.Signature 'C.Ed25519
|
||||
-- | Signature bytes, base64url encoded.
|
||||
ownerSig :: B64UrlByteString
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
|
||||
@@ -187,6 +187,7 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte
|
||||
-- CRGroupConversationsDeleted u _g _conversations -> ttyUser u []
|
||||
CRGroupsList u gs -> ttyUser u $ viewGroupsList gs
|
||||
CRSentGroupInvitation u g c _ -> ttyUser u $ viewSentGroupInvitation g c
|
||||
CRSentPublicGroupInvitation u g -> ttyUser u ["shared public group " <> ttyGroup' g]
|
||||
CRFileTransferStatus u ftStatus -> ttyUser u $ viewFileTransferStatus ftStatus
|
||||
CRFileTransferStatusXFTP u ci -> ttyUser u $ viewFileTransferStatusXFTP ci
|
||||
CRUserProfile u p -> ttyUser u $ viewUserProfile p
|
||||
|
||||
@@ -14,6 +14,8 @@ chatRelayTests = do
|
||||
it "re-add soft-deleted relay by same name" testReAddRelaySameName
|
||||
it "test chat relay" testChatRelayTest
|
||||
it "relay profile updated in address" testRelayProfileUpdateInAddress
|
||||
describe "public group invitations" $ do
|
||||
it "share public group in direct chat" testSharePublicGroupDirect
|
||||
|
||||
testGetSetChatRelays :: HasCallStack => TestParams -> IO ()
|
||||
testGetSetChatRelays ps =
|
||||
@@ -165,6 +167,22 @@ testRelayProfileUpdateInAddress ps =
|
||||
alice ##> ("/relay test " <> bobSLink)
|
||||
alice <## "relay test passed, profile: bob2 (Bob relay)"
|
||||
|
||||
testSharePublicGroupDirect :: HasCallStack => TestParams -> IO ()
|
||||
testSharePublicGroupDirect ps =
|
||||
withNewTestChat ps "alice" aliceProfile $ \alice ->
|
||||
withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob ->
|
||||
withNewTestChat ps "cath" cathProfile $ \cath -> do
|
||||
bob ##> "/ad"
|
||||
(bobSLink, _cLink) <- getContactLinks bob True
|
||||
alice ##> ("/relays name=bob_relay " <> bobSLink)
|
||||
alice <## "ok"
|
||||
createChannelWithRelay "team" alice bob
|
||||
connectUsers alice cath
|
||||
alice ##> "/share #team @cath"
|
||||
alice <## "shared public group #team"
|
||||
cath <## "#team: alice invites you to join the group as observer"
|
||||
cath <## "use /j team to accept"
|
||||
|
||||
-- Create a public group with relay=1, wait for relay to join
|
||||
createChannelWithRelay :: HasCallStack => String -> TestCC -> TestCC -> IO ()
|
||||
createChannelWithRelay gName owner relay = do
|
||||
|
||||
Reference in New Issue
Block a user