mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-08 06:46:58 +00:00
193 lines
8.4 KiB
Haskell
193 lines
8.4 KiB
Haskell
{-# LANGUAGE DuplicateRecordFields #-}
|
|
{-# LANGUAGE LambdaCase #-}
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
|
|
|
module Simplex.Chat.Core
|
|
( simplexChatCore,
|
|
runSimplexChat,
|
|
sendChatCmdStr,
|
|
sendChatCmd,
|
|
printResponseEvent,
|
|
)
|
|
where
|
|
|
|
import Control.Logger.Simple
|
|
import Control.Monad
|
|
import Control.Monad.Except
|
|
import Control.Monad.Reader
|
|
import qualified Data.ByteString.Char8 as B
|
|
import Data.List (find)
|
|
import qualified Data.Text as T
|
|
import Data.Text.Encoding (encodeUtf8)
|
|
import Data.Time.Clock (getCurrentTime)
|
|
import Data.Time.LocalTime (getCurrentTimeZone)
|
|
import Simplex.Chat
|
|
import Simplex.Chat.Controller
|
|
import Simplex.Chat.Library.Commands
|
|
import Simplex.Chat.Options (ChatOpts (..), CoreChatOpts (..), CreateBotOpts (..))
|
|
import Simplex.Chat.Remote.Types (RemoteHostId)
|
|
import Simplex.Chat.Store.Profiles
|
|
import Simplex.Chat.Store.Shared (StoreError (..))
|
|
import Simplex.Chat.Types
|
|
import Simplex.Chat.Types.Preferences (FeatureAllowed (..), FilesPreference (..), Preferences (..), emptyChatPrefs)
|
|
import Simplex.Chat.View (ChatResponseEvent, serializeChatError, serializeChatResponse, simplexChatContact)
|
|
import Simplex.Messaging.Agent.Protocol
|
|
import Simplex.Messaging.Agent.Store.Shared (MigrationConfig (..), MigrationConfirmation (..))
|
|
import Simplex.Messaging.Agent.Store.Common (DBStore, withTransaction)
|
|
import Simplex.Messaging.Encoding.String
|
|
import System.Exit (exitFailure)
|
|
import System.IO (hFlush, stdout)
|
|
import Text.Read (readMaybe)
|
|
import UnliftIO.Async
|
|
|
|
simplexChatCore :: ChatConfig -> ChatOpts -> (User -> ChatController -> IO ()) -> IO ()
|
|
simplexChatCore cfg@ChatConfig {confirmMigrations, testView, chatHooks} opts@ChatOpts {coreOptions = coreOptions@CoreChatOpts {dbOptions, logAgent, yesToUpMigrations, migrationBackupPath}, createBot, maintenance} chat =
|
|
case logAgent of
|
|
Just level -> do
|
|
setLogLevel level
|
|
withGlobalLogging logCfg initRun
|
|
_ -> initRun
|
|
where
|
|
initRun = createChatDatabase dbOptions migrationConfig >>= either exit run
|
|
migrationConfig = MigrationConfig (if confirmMigrations == MCConsole && yesToUpMigrations then MCYesUp else confirmMigrations) migrationBackupPath
|
|
exit e = do
|
|
putStrLn $ "Error opening database: " <> show e
|
|
exitFailure
|
|
run db@ChatDatabase {chatStore} = do
|
|
users <- withTransaction chatStore getUsers
|
|
u_ <- selectActiveUser coreOptions chatStore users
|
|
let backgroundMode = maintenance
|
|
cc <- newChatController db u_ cfg opts backgroundMode
|
|
u <- maybe (createActiveUser cc coreOptions createBot) pure u_
|
|
unless testView $ putStrLn $ "Current user: " <> userStr u
|
|
unless maintenance $ forM_ (preStartHook chatHooks) ($ cc)
|
|
runSimplexChat cfg opts u cc chat
|
|
|
|
runSimplexChat :: ChatConfig -> ChatOpts -> User -> ChatController -> (User -> ChatController -> IO ()) -> IO ()
|
|
runSimplexChat ChatConfig {testView} ChatOpts {coreOptions = CoreChatOpts {chatRelay}, maintenance} u cc@ChatController {config = ChatConfig {chatHooks}} chat
|
|
| maintenance = wait =<< async (chat u cc)
|
|
| otherwise = do
|
|
a1 <- runReaderT (startChatController True True) cc
|
|
when (chatRelay && not testView) $ askCreateRelayAddress cc u
|
|
forM_ (postStartHook chatHooks) ($ cc)
|
|
a2 <- async $ chat u cc
|
|
waitEither_ a1 a2
|
|
|
|
sendChatCmdStr :: ChatController -> String -> IO (Either ChatError ChatResponse)
|
|
sendChatCmdStr cc s = runReaderT (execChatCommand Nothing (encodeUtf8 $ T.pack s) 0) cc
|
|
|
|
sendChatCmd :: ChatController -> ChatCommand -> IO (Either ChatError ChatResponse)
|
|
sendChatCmd cc cmd = runReaderT (execChatCommand' cmd 0) cc
|
|
|
|
selectActiveUser :: CoreChatOpts -> DBStore -> [User] -> IO (Maybe User)
|
|
selectActiveUser CoreChatOpts {chatRelay} st users
|
|
| chatRelay =
|
|
case find (\User {userChatRelay} -> isTrue userChatRelay) users of
|
|
Just u
|
|
| activeUser u -> pure $ Just u
|
|
| otherwise -> Just <$> withTransaction st (`setActiveUser` u)
|
|
Nothing -> pure Nothing
|
|
| otherwise =
|
|
case find activeUser users of
|
|
Just u -> pure $ Just u
|
|
Nothing -> selectUser
|
|
where
|
|
selectUser :: IO (Maybe User)
|
|
selectUser = case users of
|
|
[] -> pure Nothing
|
|
[user] -> Just <$> withTransaction st (`setActiveUser` user)
|
|
_users -> do
|
|
putStrLn "Select user profile:"
|
|
forM_ (zip [1 :: Int ..] users) $ \(n, user) -> putStrLn $ show n <> ": " <> userStr user
|
|
loop
|
|
where
|
|
loop = do
|
|
nStr <- withPrompt ("user number (1 .. " <> show (length users) <> "): ") getLine
|
|
case readMaybe nStr :: Maybe Int of
|
|
Nothing -> putStrLn "not a number" >> loop
|
|
Just n
|
|
| n <= 0 || n > length users -> putStrLn "invalid user number" >> loop
|
|
| otherwise ->
|
|
let user = users !! (n - 1)
|
|
in Just <$> withTransaction st (`setActiveUser` user)
|
|
|
|
createActiveUser :: ChatController -> CoreChatOpts -> Maybe CreateBotOpts -> IO User
|
|
createActiveUser cc CoreChatOpts {chatRelay} = \case
|
|
Just CreateBotOpts {botDisplayName, allowFiles} -> do
|
|
let preferences = if allowFiles then Nothing else Just emptyChatPrefs {files = Just FilesPreference {allow = FANo}}
|
|
createUser exitFailure $ (mkProfile botDisplayName) {peerType = Just CPTBot, preferences}
|
|
Nothing
|
|
| chatRelay -> do
|
|
putStrLn
|
|
"No chat relay user profile found, it will be created now.\n\
|
|
\Please choose chat relay display name."
|
|
loop
|
|
| otherwise -> do
|
|
putStrLn
|
|
"No user profiles found, it will be created now.\n\
|
|
\Please choose your display name.\n\
|
|
\It will be sent to your contacts when you connect.\n\
|
|
\It is only stored on your device and you can change it later."
|
|
loop
|
|
where
|
|
loop = do
|
|
displayName <- T.pack <$> withPrompt "display name: " getLine
|
|
createUser loop $ mkProfile displayName
|
|
mkProfile displayName = Profile {displayName, fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing}
|
|
createUser onError p =
|
|
execChatCommand' (CreateActiveUser NewUser {profile = Just p, pastTimestamp = False, userChatRelay = chatRelay}) 0 `runReaderT` cc >>= \case
|
|
Right (CRActiveUser user) -> pure user
|
|
r -> printResponseEvent (Nothing, Nothing) (config cc) r >> onError
|
|
|
|
askCreateRelayAddress :: ChatController -> User -> IO ()
|
|
askCreateRelayAddress cc@ChatController {chatStore} user =
|
|
withTransaction chatStore (\db -> runExceptT $ getUserAddress db user) >>= \case
|
|
Right _ -> pure ()
|
|
Left SEUserContactLinkNotFound -> promptCreate
|
|
Left e -> printChatError (config cc) $ ChatErrorStore e
|
|
where
|
|
promptCreate :: IO ()
|
|
promptCreate = do
|
|
ok <- onOffPrompt "Create relay address" True
|
|
when ok $
|
|
execChatCommand' CreateMyAddress 0 `runReaderT` cc >>= \case
|
|
Right (CRUserContactLinkCreated _ address) -> do
|
|
putStrLn "Chat relay address is created:"
|
|
putStrLn $ addressStr address
|
|
r -> printResponseEvent (Nothing, Nothing) (config cc) r
|
|
addressStr :: CreatedLinkContact -> String
|
|
addressStr (CCLink cReq shortLink) = B.unpack $ maybe cReqStr strEncode shortLink
|
|
where
|
|
cReqStr = strEncode $ simplexChatContact cReq
|
|
|
|
printResponseEvent :: ChatResponseEvent r => (Maybe RemoteHostId, Maybe User) -> ChatConfig -> Either ChatError r -> IO ()
|
|
printResponseEvent hu cfg = \case
|
|
Right r -> do
|
|
ts <- getCurrentTime
|
|
tz <- getCurrentTimeZone
|
|
putStrLn $ serializeChatResponse hu cfg ts tz (fst hu) r
|
|
Left e -> printChatError cfg e
|
|
|
|
printChatError :: ChatConfig -> ChatError -> IO ()
|
|
printChatError cfg e = putStrLn $ serializeChatError True cfg e
|
|
|
|
withPrompt :: String -> IO a -> IO a
|
|
withPrompt s a = putStr s >> hFlush stdout >> a
|
|
|
|
onOffPrompt :: String -> Bool -> IO Bool
|
|
onOffPrompt prompt def =
|
|
withPrompt (prompt <> if def then " (Yn): " else " (yN): ") $
|
|
getLine >>= \case
|
|
"" -> pure def
|
|
"y" -> pure True
|
|
"Y" -> pure True
|
|
"n" -> pure False
|
|
"N" -> pure False
|
|
_ -> putStrLn "Invalid input, please enter 'y' or 'n'" >> onOffPrompt prompt def
|
|
|
|
userStr :: User -> String
|
|
userStr User {localDisplayName, profile = LocalProfile {fullName}} =
|
|
T.unpack $ localDisplayName <> if T.null fullName || localDisplayName == fullName then "" else " (" <> fullName <> ")"
|