terminal: support multiline messages (as JSON strings) (#1936)

* terminal: support for multiline messages

* fix

* fix tests
This commit is contained in:
Evgeny Poberezkin 2023-02-18 15:16:50 +00:00 committed by GitHub
parent b7575ec01d
commit 7c4c627ee9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 56 additions and 38 deletions

View file

@ -1018,7 +1018,7 @@ processChatCommand = \case
SendMessageBroadcast msg -> withUser $ \user -> do SendMessageBroadcast msg -> withUser $ \user -> do
contacts <- withStore' (`getUserContacts` user) contacts <- withStore' (`getUserContacts` user)
withChatLock "sendMessageBroadcast" . procCmd $ do withChatLock "sendMessageBroadcast" . procCmd $ do
let mc = MCText $ safeDecodeUtf8 msg let mc = MCText msg
cts = filter (\ct -> isReady ct && directOrUsed ct) contacts cts = filter (\ct -> isReady ct && directOrUsed ct) contacts
forM_ cts $ \ct -> forM_ cts $ \ct ->
void void
@ -1030,8 +1030,8 @@ processChatCommand = \case
CRBroadcastSent user mc (length cts) <$> liftIO getZonedTime CRBroadcastSent user mc (length cts) <$> liftIO getZonedTime
SendMessageQuote cName (AMsgDirection msgDir) quotedMsg msg -> withUser $ \user@User {userId} -> do SendMessageQuote cName (AMsgDirection msgDir) quotedMsg msg -> withUser $ \user@User {userId} -> do
contactId <- withStore $ \db -> getContactIdByName db user cName contactId <- withStore $ \db -> getContactIdByName db user cName
quotedItemId <- withStore $ \db -> getDirectChatItemIdByText db userId contactId msgDir (safeDecodeUtf8 quotedMsg) quotedItemId <- withStore $ \db -> getDirectChatItemIdByText db userId contactId msgDir quotedMsg
let mc = MCText $ safeDecodeUtf8 msg let mc = MCText msg
processChatCommand . APISendMessage (ChatRef CTDirect contactId) False $ ComposedMessage Nothing (Just quotedItemId) mc processChatCommand . APISendMessage (ChatRef CTDirect contactId) False $ ComposedMessage Nothing (Just quotedItemId) mc
DeleteMessage chatName deletedMsg -> withUser $ \user -> do DeleteMessage chatName deletedMsg -> withUser $ \user -> do
chatRef <- getChatRef user chatName chatRef <- getChatRef user chatName
@ -1039,16 +1039,16 @@ processChatCommand = \case
processChatCommand $ APIDeleteChatItem chatRef deletedItemId CIDMBroadcast processChatCommand $ APIDeleteChatItem chatRef deletedItemId CIDMBroadcast
DeleteMemberMessage gName mName deletedMsg -> withUser $ \user -> do DeleteMemberMessage gName mName deletedMsg -> withUser $ \user -> do
(gId, mId) <- getGroupAndMemberId user gName mName (gId, mId) <- getGroupAndMemberId user gName mName
deletedItemId <- withStore $ \db -> getGroupChatItemIdByText db user gId (Just mName) $ safeDecodeUtf8 deletedMsg deletedItemId <- withStore $ \db -> getGroupChatItemIdByText db user gId (Just mName) deletedMsg
processChatCommand $ APIDeleteMemberChatItem gId mId deletedItemId processChatCommand $ APIDeleteMemberChatItem gId mId deletedItemId
EditMessage chatName editedMsg msg -> withUser $ \user -> do EditMessage chatName editedMsg msg -> withUser $ \user -> do
chatRef <- getChatRef user chatName chatRef <- getChatRef user chatName
editedItemId <- getSentChatItemIdByText user chatRef editedMsg editedItemId <- getSentChatItemIdByText user chatRef editedMsg
let mc = MCText $ safeDecodeUtf8 msg let mc = MCText msg
processChatCommand $ APIUpdateChatItem chatRef editedItemId False mc processChatCommand $ APIUpdateChatItem chatRef editedItemId False mc
UpdateLiveMessage chatName chatItemId live msg -> withUser $ \user -> do UpdateLiveMessage chatName chatItemId live msg -> withUser $ \user -> do
chatRef <- getChatRef user chatName chatRef <- getChatRef user chatName
let mc = MCText $ safeDecodeUtf8 msg let mc = MCText msg
processChatCommand $ APIUpdateChatItem chatRef chatItemId live mc processChatCommand $ APIUpdateChatItem chatRef chatItemId live mc
APINewGroup userId gProfile -> withUserId userId $ \user -> do APINewGroup userId gProfile -> withUserId userId $ \user -> do
gVar <- asks idsDrg gVar <- asks idsDrg
@ -1217,8 +1217,8 @@ processChatCommand = \case
processChatCommand $ APIGetGroupLink groupId processChatCommand $ APIGetGroupLink groupId
SendGroupMessageQuote gName cName quotedMsg msg -> withUser $ \user -> do SendGroupMessageQuote gName cName quotedMsg msg -> withUser $ \user -> do
groupId <- withStore $ \db -> getGroupIdByName db user gName groupId <- withStore $ \db -> getGroupIdByName db user gName
quotedItemId <- withStore $ \db -> getGroupChatItemIdByText db user groupId cName (safeDecodeUtf8 quotedMsg) quotedItemId <- withStore $ \db -> getGroupChatItemIdByText db user groupId cName quotedMsg
let mc = MCText $ safeDecodeUtf8 msg let mc = MCText msg
processChatCommand . APISendMessage (ChatRef CTGroup groupId) False $ ComposedMessage Nothing (Just quotedItemId) mc processChatCommand . APISendMessage (ChatRef CTGroup groupId) False $ ComposedMessage Nothing (Just quotedItemId) mc
LastChats count_ -> withUser' $ \user -> do LastChats count_ -> withUser' $ \user -> do
chats <- withStore' $ \db -> getChatPreviews db user False chats <- withStore' $ \db -> getChatPreviews db user False
@ -1386,10 +1386,10 @@ processChatCommand = \case
code' <- getConnectionCode $ aConnId conn code' <- getConnectionCode $ aConnId conn
withStore' $ \db -> setConnectionVerified db user connId Nothing withStore' $ \db -> setConnectionVerified db user connId Nothing
pure $ CRConnectionVerified user False code' pure $ CRConnectionVerified user False code'
getSentChatItemIdByText :: User -> ChatRef -> ByteString -> m Int64 getSentChatItemIdByText :: User -> ChatRef -> Text -> m Int64
getSentChatItemIdByText user@User {userId, localDisplayName} (ChatRef cType cId) msg = case cType of getSentChatItemIdByText user@User {userId, localDisplayName} (ChatRef cType cId) msg = case cType of
CTDirect -> withStore $ \db -> getDirectChatItemIdByText db userId cId SMDSnd (safeDecodeUtf8 msg) CTDirect -> withStore $ \db -> getDirectChatItemIdByText db userId cId SMDSnd msg
CTGroup -> withStore $ \db -> getGroupChatItemIdByText db user cId (Just localDisplayName) (safeDecodeUtf8 msg) CTGroup -> withStore $ \db -> getGroupChatItemIdByText db user cId (Just localDisplayName) msg
_ -> throwChatError $ CECommandError "not supported" _ -> throwChatError $ CECommandError "not supported"
connectViaContact :: User -> ConnectionRequestUri 'CMContact -> m ChatResponse connectViaContact :: User -> ConnectionRequestUri 'CMContact -> m ChatResponse
connectViaContact user@User {userId} cReq@(CRContactUri ConnReqUriData {crClientData}) = withChatLock "connectViaContact" $ do connectViaContact user@User {userId} cReq@(CRContactUri ConnReqUriData {crClientData}) = withChatLock "connectViaContact" $ do
@ -1543,7 +1543,7 @@ processChatCommand = \case
setActive $ ActiveG localDisplayName setActive $ ActiveG localDisplayName
sendTextMessage chatName msg live = withUser $ \user -> do sendTextMessage chatName msg live = withUser $ \user -> do
chatRef <- getChatRef user chatName chatRef <- getChatRef user chatName
let mc = MCText $ safeDecodeUtf8 msg let mc = MCText msg
processChatCommand . APISendMessage chatRef live $ ComposedMessage Nothing Nothing mc processChatCommand . APISendMessage chatRef live $ ComposedMessage Nothing Nothing mc
sndContactCITimed :: Bool -> Contact -> m (Maybe CITimed) sndContactCITimed :: Bool -> Contact -> m (Maybe CITimed)
sndContactCITimed live = mapM (sndCITimed_ live) . contactTimedTTL sndContactCITimed live = mapM (sndCITimed_ live) . contactTimedTTL
@ -4056,29 +4056,29 @@ chatCommandP =
"/_group_profile #" *> (APIUpdateGroupProfile <$> A.decimal <* A.space <*> jsonP), "/_group_profile #" *> (APIUpdateGroupProfile <$> A.decimal <* A.space <*> jsonP),
("/group_profile " <|> "/gp ") *> char_ '#' *> (UpdateGroupNames <$> displayName <* A.space <*> groupProfile), ("/group_profile " <|> "/gp ") *> char_ '#' *> (UpdateGroupNames <$> displayName <* A.space <*> groupProfile),
("/group_profile " <|> "/gp ") *> char_ '#' *> (ShowGroupProfile <$> displayName), ("/group_profile " <|> "/gp ") *> char_ '#' *> (ShowGroupProfile <$> displayName),
"/group_descr " *> char_ '#' *> (UpdateGroupDescription <$> displayName <*> optional (A.space *> (jsonP <|> textP))), "/group_descr " *> char_ '#' *> (UpdateGroupDescription <$> displayName <*> optional (A.space *> msgTextP)),
"/_create link #" *> (APICreateGroupLink <$> A.decimal), "/_create link #" *> (APICreateGroupLink <$> A.decimal),
"/_delete link #" *> (APIDeleteGroupLink <$> A.decimal), "/_delete link #" *> (APIDeleteGroupLink <$> A.decimal),
"/_get link #" *> (APIGetGroupLink <$> A.decimal), "/_get link #" *> (APIGetGroupLink <$> A.decimal),
"/create link #" *> (CreateGroupLink <$> displayName), "/create link #" *> (CreateGroupLink <$> displayName),
"/delete link #" *> (DeleteGroupLink <$> displayName), "/delete link #" *> (DeleteGroupLink <$> displayName),
"/show link #" *> (ShowGroupLink <$> displayName), "/show link #" *> (ShowGroupLink <$> displayName),
(">#" <|> "> #") *> (SendGroupMessageQuote <$> displayName <* A.space <*> pure Nothing <*> quotedMsg <*> A.takeByteString), (">#" <|> "> #") *> (SendGroupMessageQuote <$> displayName <* A.space <*> pure Nothing <*> quotedMsg <*> msgTextP),
(">#" <|> "> #") *> (SendGroupMessageQuote <$> displayName <* A.space <* char_ '@' <*> (Just <$> displayName) <* A.space <*> quotedMsg <*> A.takeByteString), (">#" <|> "> #") *> (SendGroupMessageQuote <$> displayName <* A.space <* char_ '@' <*> (Just <$> displayName) <* A.space <*> quotedMsg <*> msgTextP),
"/_contacts " *> (APIListContacts <$> A.decimal), "/_contacts " *> (APIListContacts <$> A.decimal),
"/contacts" $> ListContacts, "/contacts" $> ListContacts,
"/_connect " *> (APIConnect <$> A.decimal <* A.space <*> ((Just <$> strP) <|> A.takeByteString $> Nothing)), "/_connect " *> (APIConnect <$> A.decimal <* A.space <*> ((Just <$> strP) <|> A.takeByteString $> Nothing)),
"/_connect " *> (APIAddContact <$> A.decimal), "/_connect " *> (APIAddContact <$> A.decimal),
("/connect " <|> "/c ") *> (Connect <$> ((Just <$> strP) <|> A.takeByteString $> Nothing)), ("/connect " <|> "/c ") *> (Connect <$> ((Just <$> strP) <|> A.takeByteString $> Nothing)),
("/connect" <|> "/c") $> AddContact, ("/connect" <|> "/c") $> AddContact,
SendMessage <$> chatNameP <* A.space <*> A.takeByteString, SendMessage <$> chatNameP <* A.space <*> msgTextP,
"/live " *> (SendLiveMessage <$> chatNameP <*> (A.space *> A.takeByteString <|> pure "")), "/live " *> (SendLiveMessage <$> chatNameP <*> (A.space *> msgTextP <|> pure "")),
(">@" <|> "> @") *> sendMsgQuote (AMsgDirection SMDRcv), (">@" <|> "> @") *> sendMsgQuote (AMsgDirection SMDRcv),
(">>@" <|> ">> @") *> sendMsgQuote (AMsgDirection SMDSnd), (">>@" <|> ">> @") *> sendMsgQuote (AMsgDirection SMDSnd),
("\\ " <|> "\\") *> (DeleteMessage <$> chatNameP <* A.space <*> A.takeByteString), ("\\ " <|> "\\") *> (DeleteMessage <$> chatNameP <* A.space <*> textP),
("\\\\ #" <|> "\\\\#") *> (DeleteMemberMessage <$> displayName <* A.space <* char_ '@' <*> displayName <* A.space <*> A.takeByteString), ("\\\\ #" <|> "\\\\#") *> (DeleteMemberMessage <$> displayName <* A.space <* char_ '@' <*> displayName <* A.space <*> textP),
("! " <|> "!") *> (EditMessage <$> chatNameP <* A.space <*> (quotedMsg <|> pure "") <*> A.takeByteString), ("! " <|> "!") *> (EditMessage <$> chatNameP <* A.space <*> (quotedMsg <|> pure "") <*> msgTextP),
"/feed " *> (SendMessageBroadcast <$> A.takeByteString), "/feed " *> (SendMessageBroadcast <$> msgTextP),
("/chats" <|> "/cs") *> (LastChats <$> (" all" $> Nothing <|> Just <$> (A.space *> A.decimal <|> pure 20))), ("/chats" <|> "/cs") *> (LastChats <$> (" all" $> Nothing <|> Just <$> (A.space *> A.decimal <|> pure 20))),
("/tail" <|> "/t") *> (LastMessages <$> optional (A.space *> chatNameP) <*> msgCountP <*> pure Nothing), ("/tail" <|> "/t") *> (LastMessages <$> optional (A.space *> chatNameP) <*> msgCountP <*> pure Nothing),
("/search" <|> "/?") *> (LastMessages <$> optional (A.space *> chatNameP) <*> msgCountP <*> (Just <$> (A.space *> stringP))), ("/search" <|> "/?") *> (LastMessages <$> optional (A.space *> chatNameP) <*> msgCountP <*> (Just <$> (A.space *> stringP))),
@ -4139,8 +4139,8 @@ chatCommandP =
msgContentP = "text " *> mcTextP <|> "json " *> jsonP msgContentP = "text " *> mcTextP <|> "json " *> jsonP
ciDeleteMode = "broadcast" $> CIDMBroadcast <|> "internal" $> CIDMInternal ciDeleteMode = "broadcast" $> CIDMBroadcast <|> "internal" $> CIDMInternal
displayName = safeDecodeUtf8 <$> (B.cons <$> A.satisfy refChar <*> A.takeTill (== ' ')) displayName = safeDecodeUtf8 <$> (B.cons <$> A.satisfy refChar <*> A.takeTill (== ' '))
sendMsgQuote msgDir = SendMessageQuote <$> displayName <* A.space <*> pure msgDir <*> quotedMsg <*> A.takeByteString sendMsgQuote msgDir = SendMessageQuote <$> displayName <* A.space <*> pure msgDir <*> quotedMsg <*> msgTextP
quotedMsg = A.char '(' *> A.takeTill (== ')') <* A.char ')' <* optional A.space quotedMsg = safeDecodeUtf8 <$> (A.char '(' *> A.takeTill (== ')') <* A.char ')') <* optional A.space
refChar c = c > ' ' && c /= '#' && c /= '@' refChar c = c > ' ' && c /= '#' && c /= '@'
liveMessageP = " live=" *> onOffP <|> pure False liveMessageP = " live=" *> onOffP <|> pure False
onOffP = ("on" $> True) <|> ("off" $> False) onOffP = ("on" $> True) <|> ("off" $> False)
@ -4162,6 +4162,7 @@ chatCommandP =
n <- (A.space *> A.takeByteString) <|> pure "" n <- (A.space *> A.takeByteString) <|> pure ""
pure $ if B.null n then name else safeDecodeUtf8 n pure $ if B.null n then name else safeDecodeUtf8 n
textP = safeDecodeUtf8 <$> A.takeByteString textP = safeDecodeUtf8 <$> A.takeByteString
msgTextP = jsonP <|> textP
stringP = T.unpack . safeDecodeUtf8 <$> A.takeByteString stringP = T.unpack . safeDecodeUtf8 <$> A.takeByteString
filePath = stringP filePath = stringP
memberRole = memberRole =

View file

@ -293,14 +293,14 @@ data ChatCommand
| AddressAutoAccept (Maybe AutoAccept) | AddressAutoAccept (Maybe AutoAccept)
| AcceptContact ContactName | AcceptContact ContactName
| RejectContact ContactName | RejectContact ContactName
| SendMessage ChatName ByteString | SendMessage ChatName Text
| SendLiveMessage ChatName ByteString | SendLiveMessage ChatName Text
| SendMessageQuote {contactName :: ContactName, msgDir :: AMsgDirection, quotedMsg :: ByteString, message :: ByteString} | SendMessageQuote {contactName :: ContactName, msgDir :: AMsgDirection, quotedMsg :: Text, message :: Text}
| SendMessageBroadcast ByteString -- UserId (not used in UI) | SendMessageBroadcast Text -- UserId (not used in UI)
| DeleteMessage ChatName ByteString | DeleteMessage ChatName Text
| DeleteMemberMessage GroupName ContactName ByteString | DeleteMemberMessage GroupName ContactName Text
| EditMessage {chatName :: ChatName, editedMsg :: ByteString, message :: ByteString} | EditMessage {chatName :: ChatName, editedMsg :: Text, message :: Text}
| UpdateLiveMessage {chatName :: ChatName, chatItemId :: ChatItemId, liveMessage :: Bool, message :: ByteString} | UpdateLiveMessage {chatName :: ChatName, chatItemId :: ChatItemId, liveMessage :: Bool, message :: Text}
| APINewGroup UserId GroupProfile | APINewGroup UserId GroupProfile
| NewGroup GroupProfile | NewGroup GroupProfile
| AddMember GroupName ContactName GroupMemberRole | AddMember GroupName ContactName GroupMemberRole
@ -318,7 +318,7 @@ data ChatCommand
| CreateGroupLink GroupName | CreateGroupLink GroupName
| DeleteGroupLink GroupName | DeleteGroupLink GroupName
| ShowGroupLink GroupName | ShowGroupLink GroupName
| SendGroupMessageQuote {groupName :: GroupName, contactName_ :: Maybe ContactName, quotedMsg :: ByteString, message :: ByteString} | SendGroupMessageQuote {groupName :: GroupName, contactName_ :: Maybe ContactName, quotedMsg :: Text, message :: Text}
| LastChats (Maybe Int) -- UserId (not used in UI) | LastChats (Maybe Int) -- UserId (not used in UI)
| LastMessages (Maybe ChatName) Int (Maybe String) -- UserId (not used in UI) | LastMessages (Maybe ChatName) Int (Maybe String) -- UserId (not used in UI)
| LastChatItemId (Maybe ChatName) Int -- UserId (not used in UI) | LastChatItemId (Maybe ChatName) Int -- UserId (not used in UI)

View file

@ -23,7 +23,7 @@ import Simplex.Chat.Controller
import Simplex.Chat.Messages import Simplex.Chat.Messages
import Simplex.Chat.Styled import Simplex.Chat.Styled
import Simplex.Chat.Terminal.Output import Simplex.Chat.Terminal.Output
import Simplex.Messaging.Util (safeDecodeUtf8, whenM) import Simplex.Messaging.Util (whenM)
import System.Exit (exitSuccess) import System.Exit (exitSuccess)
import System.Terminal hiding (insertChars) import System.Terminal hiding (insertChars)
import UnliftIO.STM import UnliftIO.STM
@ -61,7 +61,7 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do
startLiveMessage :: Either a ChatCommand -> ChatResponse -> IO () startLiveMessage :: Either a ChatCommand -> ChatResponse -> IO ()
startLiveMessage (Right (SendLiveMessage chatName msg)) (CRNewChatItem _ (AChatItem cType SMDSnd _ ChatItem {meta = CIMeta {itemId}})) = do startLiveMessage (Right (SendLiveMessage chatName msg)) (CRNewChatItem _ (AChatItem cType SMDSnd _ ChatItem {meta = CIMeta {itemId}})) = do
whenM (isNothing <$> readTVarIO liveMessageState) $ do whenM (isNothing <$> readTVarIO liveMessageState) $ do
let s = T.unpack $ safeDecodeUtf8 msg let s = T.unpack msg
int = case cType of SCTGroup -> 5000000; _ -> 3000000 :: Int int = case cType of SCTGroup -> 5000000; _ -> 3000000 :: Int
liveThreadId <- mkWeakThreadId =<< runLiveMessage int `forkFinally` const (atomically $ writeTVar liveMessageState Nothing) liveThreadId <- mkWeakThreadId =<< runLiveMessage int `forkFinally` const (atomically $ writeTVar liveMessageState Nothing)
promptThreadId <- mkWeakThreadId =<< forkIO blinkLivePrompt promptThreadId <- mkWeakThreadId =<< forkIO blinkLivePrompt
@ -110,8 +110,7 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do
sendUpdatedLiveMessage :: ChatController -> String -> LiveMessage -> Bool -> IO ChatResponse sendUpdatedLiveMessage :: ChatController -> String -> LiveMessage -> Bool -> IO ChatResponse
sendUpdatedLiveMessage cc sentMsg LiveMessage {chatName, chatItemId} live = do sendUpdatedLiveMessage cc sentMsg LiveMessage {chatName, chatItemId} live = do
let bs = encodeUtf8 $ T.pack sentMsg let cmd = UpdateLiveMessage chatName chatItemId live $ T.pack sentMsg
cmd = UpdateLiveMessage chatName chatItemId live bs
either (CRChatCmdError Nothing) id <$> runExceptT (processChatCommand cmd) `runReaderT` cc either (CRChatCmdError Nothing) id <$> runExceptT (processChatCommand cmd) `runReaderT` cc
runTerminalInput :: ChatTerminal -> ChatController -> IO () runTerminalInput :: ChatTerminal -> ChatController -> IO ()
@ -238,8 +237,8 @@ updateTermState ac live tw (key, ms) ts@TerminalState {inputString = s, inputPos
Left _ -> inp Left _ -> inp
Right cmd -> case cmd of Right cmd -> case cmd of
SendMessage {} -> "! " <> inp SendMessage {} -> "! " <> inp
SendMessageQuote {contactName, message} -> T.unpack $ "! @" <> contactName <> " " <> safeDecodeUtf8 message SendMessageQuote {contactName, message} -> T.unpack $ "! @" <> contactName <> " " <> message
SendGroupMessageQuote {groupName, message} -> T.unpack $ "! #" <> groupName <> " " <> safeDecodeUtf8 message SendGroupMessageQuote {groupName, message} -> T.unpack $ "! #" <> groupName <> " " <> message
_ -> inp _ -> inp
setPosition p' = ts' (s, p') setPosition p' = ts' (s, p')
prevWordPos prevWordPos

View file

@ -32,6 +32,7 @@ chatDirectTests = do
it "direct message delete" testDirectMessageDelete it "direct message delete" testDirectMessageDelete
it "direct live message" testDirectLiveMessage it "direct live message" testDirectLiveMessage
it "repeat AUTH errors disable contact" testRepeatAuthErrorsDisableContact it "repeat AUTH errors disable contact" testRepeatAuthErrorsDisableContact
it "should send multiline message" testMultilineMessage
describe "SMP servers" $ do describe "SMP servers" $ do
it "get and set SMP servers" testGetSetSMPServers it "get and set SMP servers" testGetSetSMPServers
it "test SMP server connection" testTestSMPServerConnection it "test SMP server connection" testTestSMPServerConnection
@ -369,6 +370,23 @@ testRepeatAuthErrorsDisableContact =
alice #> "@bob hey" alice #> "@bob hey"
alice <## "[bob, contactId: 2, connId: 1] error: connection authorization failed - this could happen if connection was deleted, secured with different credentials, or due to a bug - please re-create the connection" alice <## "[bob, contactId: 2, connId: 1] error: connection authorization failed - this could happen if connection was deleted, secured with different credentials, or due to a bug - please re-create the connection"
testMultilineMessage :: HasCallStack => FilePath -> IO ()
testMultilineMessage = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do
connectUsers alice bob
connectUsers alice cath
alice `send` "@bob \"hello\\nthere\"" -- @bob "hello\nthere"
alice <# "@bob hello"
alice <## "there"
bob <# "alice> hello"
bob <## "there"
alice `send` "/feed \"hello\\nthere\"" -- /feed "hello\nthere"
alice <##. "/feed (2)"
alice <## "there"
bob <# "alice> hello"
bob <## "there"
cath <# "alice> hello"
cath <## "there"
testGetSetSMPServers :: HasCallStack => FilePath -> IO () testGetSetSMPServers :: HasCallStack => FilePath -> IO ()
testGetSetSMPServers = testGetSetSMPServers =
testChat2 aliceProfile bobProfile $ testChat2 aliceProfile bobProfile $