2021-01-31 17:29:16 +00:00
|
|
|
{-# LANGUAGE DataKinds #-}
|
|
|
|
{-# LANGUAGE DuplicateRecordFields #-}
|
|
|
|
{-# LANGUAGE FlexibleContexts #-}
|
|
|
|
{-# LANGUAGE GADTs #-}
|
|
|
|
{-# LANGUAGE LambdaCase #-}
|
|
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
|
|
|
|
|
|
module Main where
|
|
|
|
|
|
|
|
import ChatOptions
|
2021-02-20 22:26:27 +00:00
|
|
|
import ChatTerminal
|
2021-01-31 17:29:16 +00:00
|
|
|
import Control.Applicative ((<|>))
|
|
|
|
import Control.Concurrent.STM
|
|
|
|
import Control.Logger.Simple
|
|
|
|
import Control.Monad.Reader
|
|
|
|
import Data.Attoparsec.ByteString.Char8 (Parser)
|
|
|
|
import qualified Data.Attoparsec.ByteString.Char8 as A
|
|
|
|
import Data.ByteString.Char8 (ByteString)
|
|
|
|
import qualified Data.ByteString.Char8 as B
|
|
|
|
import Data.Functor (($>))
|
2021-04-07 20:20:32 +01:00
|
|
|
import qualified Data.Text as T
|
|
|
|
import Data.Text.Encoding
|
2021-01-31 17:29:16 +00:00
|
|
|
import Numeric.Natural
|
2021-04-10 11:57:28 +01:00
|
|
|
import Simplex.Markdown
|
2021-01-31 17:29:16 +00:00
|
|
|
import Simplex.Messaging.Agent (getSMPAgentClient, runSMPAgentClient)
|
|
|
|
import Simplex.Messaging.Agent.Client (AgentClient (..))
|
|
|
|
import Simplex.Messaging.Agent.Env.SQLite
|
|
|
|
import Simplex.Messaging.Agent.Transmission
|
|
|
|
import Simplex.Messaging.Client (smpDefaultConfig)
|
2021-04-07 20:20:32 +01:00
|
|
|
import Simplex.Messaging.Util (raceAny_)
|
|
|
|
import Styled
|
2021-03-29 19:18:54 +04:00
|
|
|
import System.Directory (getAppUserDataDirectory)
|
2021-02-20 22:26:27 +00:00
|
|
|
import Types
|
2021-01-31 17:29:16 +00:00
|
|
|
|
|
|
|
cfg :: AgentConfig
|
|
|
|
cfg =
|
|
|
|
AgentConfig
|
|
|
|
{ tcpPort = undefined, -- TODO maybe take it out of config
|
2021-02-14 12:00:04 +00:00
|
|
|
rsaKeySize = 2048 `div` 8,
|
2021-01-31 17:29:16 +00:00
|
|
|
connIdBytes = 12,
|
2021-02-14 12:00:04 +00:00
|
|
|
tbqSize = 16,
|
2021-01-31 17:29:16 +00:00
|
|
|
dbFile = "smp-chat.db",
|
|
|
|
smpCfg = smpDefaultConfig
|
|
|
|
}
|
|
|
|
|
|
|
|
logCfg :: LogConfig
|
|
|
|
logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
|
|
|
|
|
|
|
|
data ChatClient = ChatClient
|
|
|
|
{ inQ :: TBQueue ChatCommand,
|
|
|
|
outQ :: TBQueue ChatResponse,
|
|
|
|
smpServer :: SMPServer,
|
|
|
|
username :: TVar (Maybe Contact)
|
|
|
|
}
|
|
|
|
|
|
|
|
-- | GroupMessage ChatGroup ByteString
|
|
|
|
-- | AddToGroup Contact
|
|
|
|
data ChatCommand
|
|
|
|
= ChatHelp
|
|
|
|
| AddContact Contact
|
|
|
|
| AcceptContact Contact SMPQueueInfo
|
|
|
|
| ChatWith Contact
|
|
|
|
| SetName Contact
|
|
|
|
| SendMessage Contact ByteString
|
|
|
|
|
|
|
|
chatCommandP :: Parser ChatCommand
|
|
|
|
chatCommandP =
|
|
|
|
"/help" $> ChatHelp
|
|
|
|
<|> "/add " *> (AddContact <$> contact)
|
|
|
|
<|> "/accept " *> acceptContact
|
|
|
|
<|> "/chat " *> chatWith
|
|
|
|
<|> "/name " *> setName
|
|
|
|
<|> "@" *> sendMessage
|
|
|
|
where
|
|
|
|
acceptContact = AcceptContact <$> contact <* A.space <*> smpQueueInfoP
|
|
|
|
chatWith = ChatWith <$> contact
|
|
|
|
setName = SetName <$> contact
|
|
|
|
sendMessage = SendMessage <$> contact <* A.space <*> A.takeByteString
|
|
|
|
contact = Contact <$> A.takeTill (== ' ')
|
|
|
|
|
|
|
|
data ChatResponse
|
|
|
|
= ChatHelpInfo
|
|
|
|
| Invitation SMPQueueInfo
|
|
|
|
| Connected Contact
|
|
|
|
| ReceivedMessage Contact ByteString
|
|
|
|
| Disconnected Contact
|
|
|
|
| YesYes
|
|
|
|
| ErrorInput ByteString
|
|
|
|
| ChatError AgentErrorType
|
|
|
|
| NoChatResponse
|
|
|
|
|
2021-04-07 20:20:32 +01:00
|
|
|
serializeChatResponse :: Maybe Contact -> ChatResponse -> StyledString
|
2021-01-31 17:29:16 +00:00
|
|
|
serializeChatResponse name = \case
|
|
|
|
ChatHelpInfo -> chatHelpInfo
|
2021-04-07 20:20:32 +01:00
|
|
|
Invitation qInfo -> "ask your contact to enter: /accept " <> showName name <> " " <> (bPlain . serializeSmpQueueInfo) qInfo
|
2021-01-31 17:29:16 +00:00
|
|
|
Connected c -> ttyContact c <> " connected"
|
2021-04-07 20:20:32 +01:00
|
|
|
ReceivedMessage c t -> ttyFromContact c <> " " <> msgPlain t
|
|
|
|
Disconnected c -> "disconnected from " <> ttyContact c <> " - try \"/chat " <> bPlain (toBs c) <> "\""
|
2021-01-31 17:29:16 +00:00
|
|
|
YesYes -> "you got it!"
|
2021-04-07 20:20:32 +01:00
|
|
|
ErrorInput t -> "invalid input: " <> bPlain t
|
|
|
|
ChatError e -> "chat error: " <> plain (show e)
|
2021-01-31 17:29:16 +00:00
|
|
|
NoChatResponse -> ""
|
|
|
|
where
|
|
|
|
showName Nothing = "<your name>"
|
2021-04-07 20:20:32 +01:00
|
|
|
showName (Just (Contact a)) = bPlain a
|
2021-04-08 19:32:38 +01:00
|
|
|
msgPlain = styleMarkdown . parseMarkdown . decodeUtf8With onError
|
2021-04-07 20:20:32 +01:00
|
|
|
onError _ _ = Just '?'
|
2021-01-31 17:29:16 +00:00
|
|
|
|
2021-04-07 20:20:32 +01:00
|
|
|
chatHelpInfo :: StyledString
|
2021-01-31 17:29:16 +00:00
|
|
|
chatHelpInfo =
|
|
|
|
"Using chat:\n\
|
|
|
|
\/add <name> - create invitation to send out-of-band\n\
|
|
|
|
\ to your contact <name>\n\
|
|
|
|
\ (any unique string without spaces)\n\
|
|
|
|
\/accept <name> <invitation> - accept <invitation>\n\
|
|
|
|
\ (a string that starts from \"smp::\")\n\
|
|
|
|
\ from your contact <name>\n\
|
|
|
|
\/name <name> - set <name> to use in invitations\n\
|
|
|
|
\@<name> <message> - send <message> (any string) to contact <name>\n\
|
|
|
|
\ @<name> can be omitted to send to previous"
|
|
|
|
|
|
|
|
main :: IO ()
|
|
|
|
main = do
|
2021-04-03 20:39:37 +01:00
|
|
|
ChatOpts {dbFileName, smpServer, name, termMode} <- welcomeGetOpts
|
2021-02-20 22:26:27 +00:00
|
|
|
let user = Contact <$> name
|
|
|
|
t <- getChatClient smpServer user
|
2021-03-09 07:05:08 +00:00
|
|
|
ct <- newChatTerminal (tbqSize cfg) user termMode
|
2021-01-31 17:29:16 +00:00
|
|
|
-- setLogLevel LogInfo -- LogError
|
|
|
|
-- withGlobalLogging logCfg $
|
|
|
|
env <- newSMPAgentEnv cfg {dbFile = dbFileName}
|
2021-02-20 22:26:27 +00:00
|
|
|
dogFoodChat t ct env
|
2021-01-31 17:29:16 +00:00
|
|
|
|
2021-04-03 20:39:37 +01:00
|
|
|
welcomeGetOpts :: IO ChatOpts
|
|
|
|
welcomeGetOpts = do
|
|
|
|
appDir <- getAppUserDataDirectory "simplex"
|
2021-04-10 11:57:28 +01:00
|
|
|
opts@ChatOpts {dbFileName} <- getChatOpts appDir
|
2021-04-03 20:39:37 +01:00
|
|
|
putStrLn "simpleX chat prototype"
|
|
|
|
putStrLn $ "db: " <> dbFileName
|
|
|
|
putStrLn "type \"/help\" for usage information"
|
|
|
|
pure opts
|
|
|
|
|
2021-02-20 22:26:27 +00:00
|
|
|
dogFoodChat :: ChatClient -> ChatTerminal -> Env -> IO ()
|
|
|
|
dogFoodChat t ct env = do
|
2021-01-31 17:29:16 +00:00
|
|
|
c <- runReaderT getSMPAgentClient env
|
|
|
|
raceAny_
|
|
|
|
[ runReaderT (runSMPAgentClient c) env,
|
2021-02-20 22:26:27 +00:00
|
|
|
sendToAgent t ct c,
|
|
|
|
sendToChatTerm t ct,
|
|
|
|
receiveFromAgent t ct c,
|
|
|
|
receiveFromChatTerm t ct,
|
|
|
|
chatTerminal ct
|
2021-01-31 17:29:16 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
getChatClient :: SMPServer -> Maybe Contact -> IO ChatClient
|
|
|
|
getChatClient srv name = atomically $ newChatClient (tbqSize cfg) srv name
|
|
|
|
|
|
|
|
newChatClient :: Natural -> SMPServer -> Maybe Contact -> STM ChatClient
|
|
|
|
newChatClient qSize smpServer name = do
|
|
|
|
inQ <- newTBQueue qSize
|
|
|
|
outQ <- newTBQueue qSize
|
|
|
|
username <- newTVar name
|
2021-02-20 22:26:27 +00:00
|
|
|
return ChatClient {inQ, outQ, smpServer, username}
|
2021-01-31 17:29:16 +00:00
|
|
|
|
2021-02-20 22:26:27 +00:00
|
|
|
receiveFromChatTerm :: ChatClient -> ChatTerminal -> IO ()
|
|
|
|
receiveFromChatTerm t ct = forever $ do
|
|
|
|
atomically (readTBQueue $ inputQ ct)
|
2021-04-07 20:20:32 +01:00
|
|
|
>>= processOrError . A.parseOnly (chatCommandP <* A.endOfInput) . encodeUtf8 . T.pack
|
2021-01-31 17:29:16 +00:00
|
|
|
where
|
|
|
|
processOrError = \case
|
|
|
|
Left err -> atomically . writeTBQueue (outQ t) . ErrorInput $ B.pack err
|
|
|
|
Right ChatHelp -> atomically . writeTBQueue (outQ t) $ ChatHelpInfo
|
|
|
|
Right (SetName a) -> atomically $ do
|
2021-02-20 22:26:27 +00:00
|
|
|
let user = Just a
|
|
|
|
writeTVar (username (t :: ChatClient)) user
|
|
|
|
updateUsername ct user
|
2021-01-31 17:29:16 +00:00
|
|
|
writeTBQueue (outQ t) YesYes
|
|
|
|
Right cmd -> atomically $ writeTBQueue (inQ t) cmd
|
|
|
|
|
2021-02-20 22:26:27 +00:00
|
|
|
sendToChatTerm :: ChatClient -> ChatTerminal -> IO ()
|
|
|
|
sendToChatTerm ChatClient {outQ, username} ChatTerminal {outputQ} = forever $ do
|
2021-01-31 17:29:16 +00:00
|
|
|
atomically (readTBQueue outQ) >>= \case
|
|
|
|
NoChatResponse -> return ()
|
|
|
|
resp -> do
|
|
|
|
name <- readTVarIO username
|
2021-02-20 22:26:27 +00:00
|
|
|
atomically . writeTBQueue outputQ $ serializeChatResponse name resp
|
2021-01-31 17:29:16 +00:00
|
|
|
|
2021-02-20 22:26:27 +00:00
|
|
|
sendToAgent :: ChatClient -> ChatTerminal -> AgentClient -> IO ()
|
2021-03-06 15:39:00 +04:00
|
|
|
sendToAgent ChatClient {inQ, smpServer} ct AgentClient {rcvQ} = do
|
|
|
|
atomically $ writeTBQueue rcvQ ("1", "", SUBALL) -- hack for subscribing to all
|
2021-01-31 17:29:16 +00:00
|
|
|
forever . atomically $ do
|
|
|
|
cmd <- readTBQueue inQ
|
|
|
|
writeTBQueue rcvQ `mapM_` agentTransmission cmd
|
|
|
|
setActiveContact cmd
|
|
|
|
where
|
|
|
|
setActiveContact :: ChatCommand -> STM ()
|
|
|
|
setActiveContact cmd =
|
2021-02-20 22:26:27 +00:00
|
|
|
writeTVar (activeContact ct) $ case cmd of
|
2021-01-31 17:29:16 +00:00
|
|
|
ChatWith a -> Just a
|
|
|
|
SendMessage a _ -> Just a
|
|
|
|
_ -> Nothing
|
|
|
|
agentTransmission :: ChatCommand -> Maybe (ATransmission 'Client)
|
|
|
|
agentTransmission = \case
|
|
|
|
AddContact a -> transmission a $ NEW smpServer
|
|
|
|
AcceptContact a qInfo -> transmission a $ JOIN qInfo $ ReplyVia smpServer
|
|
|
|
ChatWith a -> transmission a SUB
|
|
|
|
SendMessage a msg -> transmission a $ SEND msg
|
|
|
|
ChatHelp -> Nothing
|
|
|
|
SetName _ -> Nothing
|
|
|
|
transmission :: Contact -> ACommand 'Client -> Maybe (ATransmission 'Client)
|
|
|
|
transmission (Contact a) cmd = Just ("1", a, cmd)
|
|
|
|
|
2021-02-20 22:26:27 +00:00
|
|
|
receiveFromAgent :: ChatClient -> ChatTerminal -> AgentClient -> IO ()
|
|
|
|
receiveFromAgent t ct c = forever . atomically $ do
|
2021-01-31 17:29:16 +00:00
|
|
|
resp <- chatResponse <$> readTBQueue (sndQ c)
|
|
|
|
writeTBQueue (outQ t) resp
|
|
|
|
setActiveContact resp
|
|
|
|
where
|
|
|
|
chatResponse :: ATransmission 'Agent -> ChatResponse
|
|
|
|
chatResponse (_, a, resp) = case resp of
|
|
|
|
INV qInfo -> Invitation qInfo
|
2021-04-07 20:20:32 +01:00
|
|
|
CON -> Connected contact
|
|
|
|
END -> Disconnected contact
|
|
|
|
MSG {m_body} -> ReceivedMessage contact m_body
|
2021-01-31 17:29:16 +00:00
|
|
|
SENT _ -> NoChatResponse
|
2021-04-07 20:20:32 +01:00
|
|
|
OK -> Connected contact -- hack for subscribing to all
|
2021-01-31 17:29:16 +00:00
|
|
|
ERR e -> ChatError e
|
2021-04-07 20:20:32 +01:00
|
|
|
where
|
|
|
|
contact = Contact a
|
2021-01-31 17:29:16 +00:00
|
|
|
setActiveContact :: ChatResponse -> STM ()
|
|
|
|
setActiveContact = \case
|
|
|
|
Connected a -> set $ Just a
|
|
|
|
ReceivedMessage a _ -> set $ Just a
|
|
|
|
Disconnected _ -> set Nothing
|
|
|
|
_ -> return ()
|
|
|
|
where
|
2021-02-20 22:26:27 +00:00
|
|
|
set a = writeTVar (activeContact ct) a
|