2021-07-06 19:07:03 +01:00
{- # LANGUAGE DataKinds # -}
2021-07-04 18:42:24 +01:00
{- # LANGUAGE DuplicateRecordFields # -}
2021-06-25 18:18:24 +01:00
{- # LANGUAGE FlexibleContexts # -}
{- # LANGUAGE GADTs # -}
{- # LANGUAGE LambdaCase # -}
{- # LANGUAGE NamedFieldPuns # -}
{- # LANGUAGE OverloadedStrings # -}
2021-07-04 18:42:24 +01:00
{- # LANGUAGE RankNTypes # -}
2021-08-22 15:56:36 +01:00
{- # LANGUAGE RecordWildCards # -}
2021-07-04 18:42:24 +01:00
{- # LANGUAGE ScopedTypeVariables # -}
2021-07-25 20:23:52 +01:00
{- # LANGUAGE TupleSections # -}
2021-06-25 18:18:24 +01:00
module Simplex.Chat where
2021-09-04 07:32:56 +01:00
import Control.Applicative ( optional , ( <|> ) )
import Control.Concurrent.STM ( stateTVar )
2021-07-07 22:46:38 +01:00
import Control.Logger.Simple
2021-06-25 18:18:24 +01:00
import Control.Monad.Except
import Control.Monad.IO.Unlift
import Control.Monad.Reader
2021-07-12 19:00:03 +01:00
import Crypto.Random ( drgNew )
import Data.Attoparsec.ByteString.Char8 ( Parser )
2021-06-25 18:18:24 +01:00
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Bifunctor ( first )
import Data.ByteString.Char8 ( ByteString )
2021-07-11 12:22:22 +01:00
import qualified Data.ByteString.Char8 as B
2021-06-25 18:18:24 +01:00
import Data.Functor ( ( $> ) )
2021-09-04 07:32:56 +01:00
import Data.Int ( Int64 )
2021-07-04 18:42:24 +01:00
import Data.List ( find )
2021-09-04 07:32:56 +01:00
import Data.Map.Strict ( Map )
import qualified Data.Map.Strict as M
2021-07-25 20:23:52 +01:00
import Data.Maybe ( isJust , mapMaybe )
2021-07-04 18:42:24 +01:00
import Data.Text ( Text )
2021-06-25 18:18:24 +01:00
import qualified Data.Text as T
import Data.Text.Encoding ( encodeUtf8 )
import Simplex.Chat.Controller
2021-07-05 20:05:07 +01:00
import Simplex.Chat.Help
2021-07-07 22:46:38 +01:00
import Simplex.Chat.Input
2021-07-05 20:05:07 +01:00
import Simplex.Chat.Notification
2021-07-07 22:46:38 +01:00
import Simplex.Chat.Options ( ChatOpts ( .. ) )
2021-07-04 18:42:24 +01:00
import Simplex.Chat.Protocol
2021-07-05 20:05:07 +01:00
import Simplex.Chat.Store
2021-06-25 18:18:24 +01:00
import Simplex.Chat.Styled ( plain )
2021-07-05 20:05:07 +01:00
import Simplex.Chat.Terminal
2021-07-04 18:42:24 +01:00
import Simplex.Chat.Types
2021-09-04 07:32:56 +01:00
import Simplex.Chat.Util ( ifM , unlessM )
2021-07-05 20:05:07 +01:00
import Simplex.Chat.View
2021-06-25 18:18:24 +01:00
import Simplex.Messaging.Agent
2021-08-02 20:10:24 +01:00
import Simplex.Messaging.Agent.Env.SQLite ( AgentConfig ( .. ) , defaultAgentConfig )
2021-06-25 18:18:24 +01:00
import Simplex.Messaging.Agent.Protocol
2021-07-27 08:08:05 +01:00
import qualified Simplex.Messaging.Crypto as C
2021-06-25 18:18:24 +01:00
import Simplex.Messaging.Parsers ( parseAll )
2021-09-04 07:32:56 +01:00
import qualified Simplex.Messaging.Protocol as SMP
2021-09-05 14:08:29 +01:00
import Simplex.Messaging.Util ( bshow , raceAny_ , tryError )
2021-07-27 08:08:05 +01:00
import System.Exit ( exitFailure , exitSuccess )
2021-09-04 07:32:56 +01:00
import System.FilePath ( combine , splitExtensions , takeFileName )
import System.IO ( Handle , IOMode ( .. ) , SeekMode ( .. ) , hFlush , openFile , stdout )
2021-07-05 19:54:44 +01:00
import Text.Read ( readMaybe )
2021-07-07 22:46:38 +01:00
import UnliftIO.Async ( race_ )
2021-09-04 07:32:56 +01:00
import UnliftIO.Concurrent ( forkIO , threadDelay )
import UnliftIO.Directory ( doesDirectoryExist , doesFileExist , getFileSize , getHomeDirectory , getTemporaryDirectory )
2021-08-05 20:51:48 +01:00
import qualified UnliftIO.Exception as E
2021-09-04 07:32:56 +01:00
import UnliftIO.IO ( hClose , hSeek , hTell )
2021-06-25 18:18:24 +01:00
import UnliftIO.STM
data ChatCommand
= ChatHelp
2021-09-05 05:38:11 +10:00
| FilesHelp
| GroupsHelp
2021-06-25 18:18:24 +01:00
| MarkdownHelp
2021-07-05 19:54:44 +01:00
| AddContact
2021-12-02 11:17:09 +00:00
| Connect ConnectionRequest
2021-07-14 20:11:41 +01:00
| DeleteContact ContactName
| SendMessage ContactName ByteString
2021-07-12 19:00:03 +01:00
| NewGroup GroupProfile
2021-07-14 20:11:41 +01:00
| AddMember GroupName ContactName GroupMemberRole
2021-07-16 07:40:55 +01:00
| JoinGroup GroupName
2021-07-14 20:11:41 +01:00
| RemoveMember GroupName ContactName
| MemberRole GroupName ContactName GroupMemberRole
| LeaveGroup GroupName
| DeleteGroup GroupName
| ListMembers GroupName
| SendGroupMessage GroupName ByteString
2021-09-04 07:32:56 +01:00
| SendFile ContactName FilePath
| SendGroupFile GroupName FilePath
| ReceiveFile Int64 ( Maybe FilePath )
| CancelFile Int64
| FileStatus Int64
2021-08-22 15:56:36 +01:00
| UpdateProfile Profile
| ShowProfile
2021-07-27 08:08:05 +01:00
| QuitChat
2021-11-07 21:57:05 +00:00
| ShowVersion
2021-06-25 18:18:24 +01:00
deriving ( Show )
2021-08-02 20:10:24 +01:00
defaultChatConfig :: ChatConfig
defaultChatConfig =
ChatConfig
{ agentConfig =
defaultAgentConfig
{ tcpPort = undefined , -- agent does not listen to TCP
smpServers = undefined , -- filled in from options
dbFile = undefined , -- filled in from options
dbPoolSize = 1
} ,
2021-07-24 10:26:28 +01:00
dbPoolSize = 1 ,
2021-09-04 07:32:56 +01:00
tbqSize = 16 ,
fileChunkSize = 7050
2021-07-07 22:46:38 +01:00
}
logCfg :: LogConfig
logCfg = LogConfig { lc_file = Nothing , lc_stderr = True }
2021-08-02 20:10:24 +01:00
simplexChat :: WithTerminal t => ChatConfig -> ChatOpts -> t -> IO ()
simplexChat cfg opts t =
2021-07-07 22:46:38 +01:00
-- setLogLevel LogInfo -- LogError
-- withGlobalLogging logCfg $ do
initializeNotifications
2021-08-02 20:10:24 +01:00
>>= newChatController cfg opts t
2021-07-07 22:46:38 +01:00
>>= runSimplexChat
2021-08-02 20:10:24 +01:00
newChatController :: WithTerminal t => ChatConfig -> ChatOpts -> t -> ( Notification -> IO () ) -> IO ChatController
2021-09-04 07:32:56 +01:00
newChatController config @ ChatConfig { agentConfig = cfg , dbPoolSize , tbqSize } ChatOpts { dbFile , smpServers } t sendNotification = do
2021-08-02 20:10:24 +01:00
chatStore <- createStore ( dbFile <> " .chat.db " ) dbPoolSize
2021-08-22 15:56:36 +01:00
currentUser <- newTVarIO =<< getCreateActiveUser chatStore
2021-07-07 22:46:38 +01:00
chatTerminal <- newChatTerminal t
smpAgent <- getSMPAgentClient cfg { dbFile = dbFile <> " .agent.db " , smpServers }
2021-07-12 19:00:03 +01:00
idsDrg <- newTVarIO =<< drgNew
2021-08-02 20:10:24 +01:00
inputQ <- newTBQueueIO tbqSize
notifyQ <- newTBQueueIO tbqSize
2021-08-05 20:51:48 +01:00
chatLock <- newTMVarIO ()
2021-09-04 07:32:56 +01:00
sndFiles <- newTVarIO M . empty
rcvFiles <- newTVarIO M . empty
2021-08-22 15:56:36 +01:00
pure ChatController { .. }
2021-07-07 22:46:38 +01:00
runSimplexChat :: ChatController -> IO ()
runSimplexChat = runReaderT ( race_ runTerminalInput runChatController )
2021-06-25 18:18:24 +01:00
runChatController :: ( MonadUnliftIO m , MonadReader ChatController m ) => m ()
2021-06-26 20:20:33 +01:00
runChatController =
raceAny_
[ inputSubscriber ,
agentSubscriber ,
notificationSubscriber
]
2021-06-25 18:18:24 +01:00
2021-08-05 20:51:48 +01:00
withLock :: MonadUnliftIO m => TMVar () -> m () -> m ()
withLock lock =
E . bracket_
( void . atomically $ takeTMVar lock )
( atomically $ putTMVar lock () )
2021-06-25 18:18:24 +01:00
inputSubscriber :: ( MonadUnliftIO m , MonadReader ChatController m ) => m ()
inputSubscriber = do
q <- asks inputQ
2021-08-05 20:51:48 +01:00
l <- asks chatLock
2021-09-04 07:32:56 +01:00
a <- asks smpAgent
2021-06-25 18:18:24 +01:00
forever $
atomically ( readTBQueue q ) >>= \ case
InputControl _ -> pure ()
InputCommand s ->
case parseAll chatCommandP . encodeUtf8 $ T . pack s of
Left e -> printToView [ plain s , " invalid input: " <> plain e ]
Right cmd -> do
case cmd of
SendMessage c msg -> showSentMessage c msg
2021-07-24 10:26:28 +01:00
SendGroupMessage g msg -> showSentGroupMessage g msg
2021-09-05 14:08:29 +01:00
SendFile c f -> showSentFileInvitation c f
SendGroupFile g f -> showSentGroupFileInvitation g f
2021-06-25 18:18:24 +01:00
_ -> printToView [ plain s ]
2021-08-22 15:56:36 +01:00
user <- readTVarIO =<< asks currentUser
2021-09-04 07:32:56 +01:00
withAgentLock a . withLock l . void . runExceptT $
2021-08-05 20:51:48 +01:00
processChatCommand user cmd ` catchError ` showChatError
2021-06-25 18:18:24 +01:00
2021-09-04 07:32:56 +01:00
processChatCommand :: forall m . ChatMonad m => User -> ChatCommand -> m ()
2021-07-12 19:00:03 +01:00
processChatCommand user @ User { userId , profile } = \ case
2021-06-25 18:18:24 +01:00
ChatHelp -> printToView chatHelpInfo
2021-09-05 05:38:11 +10:00
FilesHelp -> printToView filesHelpInfo
GroupsHelp -> printToView groupsHelpInfo
2021-06-25 18:18:24 +01:00
MarkdownHelp -> printToView markdownInfo
2021-07-05 19:54:44 +01:00
AddContact -> do
2021-12-02 11:17:09 +00:00
( connId , cReq ) <- withAgent createConnection
2021-07-05 19:54:44 +01:00
withStore $ \ st -> createDirectConnection st userId connId
2021-12-02 11:17:09 +00:00
showInvitation cReq
Connect cReq -> do
connId <- withAgent $ \ a -> joinConnection a cReq . directMessage $ XInfo profile
2021-07-05 19:54:44 +01:00
withStore $ \ st -> createDirectConnection st userId connId
2021-08-02 20:10:24 +01:00
DeleteContact cName ->
withStore ( \ st -> getContactGroupNames st userId cName ) >>= \ case
[] -> do
conns <- withStore $ \ st -> getContactConnections st userId cName
withAgent $ \ a -> forM_ conns $ \ Connection { agentConnId } ->
deleteConnection a agentConnId ` catchError ` \ ( _ :: AgentErrorType ) -> pure ()
withStore $ \ st -> deleteContact st userId cName
unsetActive $ ActiveC cName
showContactDeleted cName
gs -> showContactGroups cName gs
2021-07-16 07:40:55 +01:00
SendMessage cName msg -> do
contact <- withStore $ \ st -> getContact st userId cName
2021-07-24 10:26:28 +01:00
let msgEvent = XMsgNew $ MsgContent MTText [] [ MsgContentBody { contentType = SimplexContentType XCText , contentData = msg } ]
2021-07-16 07:40:55 +01:00
sendDirectMessage ( contactConnId contact ) msgEvent
setActive $ ActiveC cName
2021-07-12 19:00:03 +01:00
NewGroup gProfile -> do
gVar <- asks idsDrg
2021-07-16 07:40:55 +01:00
group <- withStore $ \ st -> createNewGroup st gVar user gProfile
showGroupCreated group
AddMember gName cName memRole -> do
( group , contact ) <- withStore $ \ st -> ( , ) <$> getGroup st user gName <*> getContact st userId cName
2021-07-12 19:00:03 +01:00
let Group { groupId , groupProfile , membership , members } = group
userRole = memberRole membership
userMemberId = memberId membership
2021-08-02 20:10:24 +01:00
when ( userRole < GRAdmin || userRole < memRole ) $ chatError CEGroupUserRole
when ( memberStatus membership == GSMemInvited ) $ chatError ( CEGroupNotJoined gName )
unless ( memberActive membership ) $ chatError CEGroupMemberNotActive
when ( isJust $ contactMember contact members ) $ chatError ( CEGroupDuplicateMember cName )
2021-07-12 19:00:03 +01:00
gVar <- asks idsDrg
2021-12-02 11:17:09 +00:00
( agentConnId , cReq ) <- withAgent createConnection
2021-07-24 10:26:28 +01:00
GroupMember { memberId } <- withStore $ \ st -> createContactGroupMember st gVar user groupId contact memRole agentConnId
2021-12-02 11:17:09 +00:00
let msg = XGrpInv $ GroupInvitation ( userMemberId , userRole ) ( memberId , memRole ) cReq groupProfile
2021-07-16 07:40:55 +01:00
sendDirectMessage ( contactConnId contact ) msg
2021-08-02 20:10:24 +01:00
showSentGroupInvitation gName cName
2021-07-24 10:26:28 +01:00
setActive $ ActiveG gName
2021-07-16 07:40:55 +01:00
JoinGroup gName -> do
2021-12-02 11:17:09 +00:00
ReceivedGroupInvitation { fromMember , userMember , connRequest } <- withStore $ \ st -> getGroupInvitation st user gName
agentConnId <- withAgent $ \ a -> joinConnection a connRequest . directMessage . XGrpAcpt $ memberId userMember
2021-07-24 10:26:28 +01:00
withStore $ \ st -> do
2021-08-02 20:10:24 +01:00
createMemberConnection st userId fromMember agentConnId
updateGroupMemberStatus st userId fromMember GSMemAccepted
updateGroupMemberStatus st userId userMember GSMemAccepted
2021-07-24 10:26:28 +01:00
MemberRole _gName _cName _mRole -> pure ()
2021-08-02 20:10:24 +01:00
RemoveMember gName cName -> do
Group { membership , members } <- withStore $ \ st -> getGroup st user gName
case find ( ( == cName ) . ( localDisplayName :: GroupMember -> ContactName ) ) members of
Nothing -> chatError $ CEGroupMemberNotFound cName
Just member -> do
let userRole = memberRole membership
when ( userRole < GRAdmin || userRole < memberRole member ) $ chatError CEGroupUserRole
sendGroupMessage members . XGrpMemDel $ memberId member
deleteMemberConnection member
withStore $ \ st -> updateGroupMemberStatus st userId member GSMemRemoved
showDeletedMember gName Nothing ( Just member )
LeaveGroup gName -> do
Group { membership , members } <- withStore $ \ st -> getGroup st user gName
sendGroupMessage members XGrpLeave
mapM_ deleteMemberConnection members
withStore $ \ st -> updateGroupMemberStatus st userId membership GSMemLeft
showLeftMemberUser gName
DeleteGroup gName -> do
g @ Group { membership , members } <- withStore $ \ st -> getGroup st user gName
let s = memberStatus membership
canDelete =
memberRole membership == GROwner
|| ( s == GSMemRemoved || s == GSMemLeft || s == GSMemGroupDeleted )
unless canDelete $ chatError CEGroupUserRole
when ( memberActive membership ) $ sendGroupMessage members XGrpDel
mapM_ deleteMemberConnection members
withStore $ \ st -> deleteGroup st user g
showGroupDeletedUser gName
2021-07-27 08:08:05 +01:00
ListMembers gName -> do
group <- withStore $ \ st -> getGroup st user gName
showGroupMembers group
2021-07-24 10:26:28 +01:00
SendGroupMessage gName msg -> do
-- TODO save sent messages
-- TODO save pending message delivery for members without connections
2021-08-02 20:10:24 +01:00
Group { members , membership } <- withStore $ \ st -> getGroup st user gName
unless ( memberActive membership ) $ chatError CEGroupMemberUserRemoved
2021-07-24 10:26:28 +01:00
let msgEvent = XMsgNew $ MsgContent MTText [] [ MsgContentBody { contentType = SimplexContentType XCText , contentData = msg } ]
sendGroupMessage members msgEvent
setActive $ ActiveG gName
2021-09-04 07:32:56 +01:00
SendFile cName f -> do
2021-09-05 14:08:29 +01:00
( fileSize , chSize ) <- checkSndFile f
contact <- withStore $ \ st -> getContact st userId cName
2021-12-02 11:17:09 +00:00
( agentConnId , fileConnReq ) <- withAgent createConnection
let fileInv = FileInvitation { fileName = takeFileName f , fileSize , fileConnReq }
2021-09-05 14:08:29 +01:00
SndFileTransfer { fileId } <- withStore $ \ st ->
createSndFileTransfer st userId contact f fileInv agentConnId chSize
2021-09-04 07:32:56 +01:00
sendDirectMessage ( contactConnId contact ) $ XFile fileInv
2021-09-05 14:08:29 +01:00
showSentFileInfo fileId
2021-09-04 07:32:56 +01:00
setActive $ ActiveC cName
2021-09-05 14:08:29 +01:00
SendGroupFile gName f -> do
( fileSize , chSize ) <- checkSndFile f
group @ Group { members , membership } <- withStore $ \ st -> getGroup st user gName
unless ( memberActive membership ) $ chatError CEGroupMemberUserRemoved
let fileName = takeFileName f
ms <- forM ( filter memberActive members ) $ \ m -> do
2021-12-02 11:17:09 +00:00
( connId , fileConnReq ) <- withAgent createConnection
pure ( m , connId , FileInvitation { fileName , fileSize , fileConnReq } )
2021-09-05 14:08:29 +01:00
fileId <- withStore $ \ st -> createSndGroupFileTransfer st userId group ms f fileSize chSize
forM_ ms $ \ ( m , _ , fileInv ) ->
traverse ( ` sendDirectMessage ` XFile fileInv ) $ memberConnId m
showSentFileInfo fileId
setActive $ ActiveG gName
2021-09-04 07:32:56 +01:00
ReceiveFile fileId filePath_ -> do
2021-12-02 11:17:09 +00:00
ft @ RcvFileTransfer { fileInvitation = FileInvitation { fileName , fileConnReq } , fileStatus } <- withStore $ \ st -> getRcvFileTransfer st userId fileId
2021-09-04 07:32:56 +01:00
unless ( fileStatus == RFSNew ) . chatError $ CEFileAlreadyReceiving fileName
2021-12-02 11:17:09 +00:00
tryError ( withAgent $ \ a -> joinConnection a fileConnReq . directMessage $ XFileAcpt fileName ) >>= \ case
2021-09-05 14:08:29 +01:00
Right agentConnId -> do
filePath <- getRcvFilePath fileId filePath_ fileName
withStore $ \ st -> acceptRcvFileTransfer st userId fileId agentConnId filePath
showRcvFileAccepted ft filePath
Left ( ChatErrorAgent ( SMP SMP . AUTH ) ) -> showRcvFileSndCancelled ft
Left ( ChatErrorAgent ( CONN DUPLICATE ) ) -> showRcvFileSndCancelled ft
Left e -> throwError e
2021-09-04 07:32:56 +01:00
CancelFile fileId ->
withStore ( \ st -> getFileTransfer st userId fileId ) >>= \ case
FTSnd fts -> do
2021-09-05 14:08:29 +01:00
forM_ fts $ \ ft -> cancelSndFileTransfer ft
showSndGroupFileCancelled fts
2021-09-04 07:32:56 +01:00
FTRcv ft -> do
cancelRcvFileTransfer ft
2021-09-05 14:08:29 +01:00
showRcvFileCancelled ft
2021-09-04 07:32:56 +01:00
FileStatus fileId ->
withStore ( \ st -> getFileTransferProgress st userId fileId ) >>= showFileTransferStatus
2021-08-22 15:56:36 +01:00
UpdateProfile p -> unless ( p == profile ) $ do
user' <- withStore $ \ st -> updateUserProfile st user p
asks currentUser >>= atomically . ( ` writeTVar ` user' )
contacts <- withStore ( ` getUserContacts ` user )
forM_ contacts $ \ ct -> sendDirectMessage ( contactConnId ct ) $ XInfo p
showUserProfileUpdated user user'
ShowProfile -> showUserProfile profile
2021-07-27 08:08:05 +01:00
QuitChat -> liftIO exitSuccess
2021-11-07 21:57:05 +00:00
ShowVersion -> printToView clientVersionInfo
2021-07-12 19:00:03 +01:00
where
2021-08-02 20:10:24 +01:00
contactMember :: Contact -> [ GroupMember ] -> Maybe GroupMember
contactMember Contact { contactId } =
find $ \ GroupMember { memberContactId = cId , memberStatus = s } ->
cId == Just contactId && s /= GSMemRemoved && s /= GSMemLeft
2021-09-05 14:08:29 +01:00
checkSndFile :: FilePath -> m ( Integer , Integer )
checkSndFile f = do
unlessM ( doesFileExist f ) . chatError $ CEFileNotFound f
( , ) <$> getFileSize f <*> asks ( fileChunkSize . config )
2021-09-04 07:32:56 +01:00
getRcvFilePath :: Int64 -> Maybe FilePath -> String -> m FilePath
getRcvFilePath fileId filePath fileName = case filePath of
Nothing -> do
dir <- ( ` combine ` " Downloads " ) <$> getHomeDirectory
ifM ( doesDirectoryExist dir ) ( pure dir ) getTemporaryDirectory
>>= ( ` uniqueCombine ` fileName )
>>= createEmptyFile
Just fPath ->
ifM
( doesDirectoryExist fPath )
( fPath ` uniqueCombine ` fileName >>= createEmptyFile )
$ ifM
( doesFileExist fPath )
( chatError $ CEFileAlreadyExists fPath )
( createEmptyFile fPath )
where
createEmptyFile :: FilePath -> m FilePath
createEmptyFile fPath = emptyFile fPath ` E . catch ` ( chatError . CEFileWrite fPath )
emptyFile :: FilePath -> m FilePath
emptyFile fPath = do
h <- getFileHandle fileId fPath rcvFiles AppendMode
liftIO $ B . hPut h " " >> hFlush h
pure fPath
uniqueCombine :: FilePath -> String -> m FilePath
uniqueCombine filePath fileName = tryCombine ( 0 :: Int )
where
tryCombine n =
let ( name , ext ) = splitExtensions fileName
suffix = if n == 0 then " " else " _ " <> show n
f = filePath ` combine ` ( name <> suffix <> ext )
in ifM ( doesFileExist f ) ( tryCombine $ n + 1 ) ( pure f )
2021-06-25 18:18:24 +01:00
agentSubscriber :: ( MonadUnliftIO m , MonadReader ChatController m ) => m ()
agentSubscriber = do
2021-07-06 19:07:03 +01:00
q <- asks $ subQ . smpAgent
2021-08-05 20:51:48 +01:00
l <- asks chatLock
2021-07-25 20:23:52 +01:00
subscribeUserConnections
2021-06-25 18:18:24 +01:00
forever $ do
2021-07-06 19:07:03 +01:00
( _ , connId , msg ) <- atomically $ readTBQueue q
2021-08-22 15:56:36 +01:00
user <- readTVarIO =<< asks currentUser
2021-08-05 20:51:48 +01:00
withLock l . void . runExceptT $
2021-09-04 07:32:56 +01:00
processAgentMessage user connId msg ` catchError ` showChatError
2021-07-04 18:42:24 +01:00
2021-07-25 20:23:52 +01:00
subscribeUserConnections :: ( MonadUnliftIO m , MonadReader ChatController m ) => m ()
subscribeUserConnections = void . runExceptT $ do
2021-08-22 15:56:36 +01:00
user <- readTVarIO =<< asks currentUser
2021-07-25 20:23:52 +01:00
subscribeContacts user
subscribeGroups user
2021-09-04 07:32:56 +01:00
subscribeFiles user
2021-08-28 20:54:53 +10:00
subscribePendingConnections user
2021-07-25 20:23:52 +01:00
where
subscribeContacts user = do
contacts <- withStore ( ` getUserContacts ` user )
forM_ contacts $ \ ct @ Contact { localDisplayName = c } ->
( subscribe ( contactConnId ct ) >> showContactSubscribed c ) ` catchError ` showContactSubError c
subscribeGroups user = do
2021-08-02 20:10:24 +01:00
groups <- withStore ( ` getUserGroups ` user )
forM_ groups $ \ Group { members , membership , localDisplayName = g } -> do
2021-07-25 20:23:52 +01:00
let connectedMembers = mapMaybe ( \ m -> ( m , ) <$> memberConnId m ) members
2021-08-02 20:10:24 +01:00
if null connectedMembers
then
if memberActive membership
then showGroupEmpty g
else showGroupRemoved g
else do
forM_ connectedMembers $ \ ( GroupMember { localDisplayName = c } , cId ) ->
subscribe cId ` catchError ` showMemberSubError g c
showGroupSubscribed g
2021-09-04 07:32:56 +01:00
subscribeFiles user = do
withStore ( ` getLiveSndFileTransfers ` user ) >>= mapM_ subscribeSndFile
withStore ( ` getLiveRcvFileTransfers ` user ) >>= mapM_ subscribeRcvFile
where
subscribeSndFile ft @ SndFileTransfer { fileId , fileStatus , agentConnId } = do
subscribe agentConnId ` catchError ` showSndFileSubError ft
void . forkIO $ do
threadDelay 1000000
l <- asks chatLock
a <- asks smpAgent
unless ( fileStatus == FSNew ) . unlessM ( isFileActive fileId sndFiles ) $
withAgentLock a . withLock l $
sendFileChunk ft
subscribeRcvFile ft @ RcvFileTransfer { fileStatus } =
case fileStatus of
RFSAccepted fInfo -> resume fInfo
RFSConnected fInfo -> resume fInfo
_ -> pure ()
where
resume RcvFileInfo { agentConnId } =
subscribe agentConnId ` catchError ` showRcvFileSubError ft
2021-08-28 20:54:53 +10:00
subscribePendingConnections user = do
connections <- withStore ( ` getPendingConnections ` user )
forM_ connections $ \ Connection { agentConnId } ->
subscribe agentConnId ` catchError ` \ _ -> pure ()
2021-07-25 20:23:52 +01:00
subscribe cId = withAgent ( ` subscribeConnection ` cId )
2021-07-06 19:07:03 +01:00
processAgentMessage :: forall m . ChatMonad m => User -> ConnId -> ACommand 'Agent -> m ()
2021-09-04 07:32:56 +01:00
processAgentMessage user @ User { userId , profile } agentConnId agentMessage = do
2021-07-16 07:40:55 +01:00
chatDirection <- withStore $ \ st -> getConnectionChatDirection st user agentConnId
2021-07-24 18:11:04 +01:00
forM_ ( agentMsgConnStatus agentMessage ) $ \ status ->
withStore $ \ st -> updateConnectionStatus st ( fromConnection chatDirection ) status
2021-07-06 19:07:03 +01:00
case chatDirection of
2021-07-24 18:11:04 +01:00
ReceivedDirectMessage conn maybeContact ->
processDirectMessage agentMessage conn maybeContact
ReceivedGroupMessage conn gName m ->
processGroupMessage agentMessage conn gName m
2021-09-04 07:32:56 +01:00
RcvFileConnection conn ft ->
processRcvFileConn agentMessage conn ft
SndFileConnection conn ft ->
processSndFileConn agentMessage conn ft
2021-07-24 18:11:04 +01:00
where
isMember :: MemberId -> Group -> Bool
isMember memId Group { membership , members } =
memberId membership == memId || isJust ( find ( ( == memId ) . memberId ) members )
contactIsReady :: Contact -> Bool
contactIsReady Contact { activeConn } = connStatus activeConn == ConnReady
memberIsReady :: GroupMember -> Bool
memberIsReady GroupMember { activeConn } = maybe False ( ( == ConnReady ) . connStatus ) activeConn
agentMsgConnStatus :: ACommand 'Agent -> Maybe ConnStatus
agentMsgConnStatus = \ case
2021-08-05 08:38:39 +01:00
REQ _ _ -> Just ConnRequested
2021-07-24 18:11:04 +01:00
INFO _ -> Just ConnSndReady
CON -> Just ConnReady
_ -> Nothing
processDirectMessage :: ACommand 'Agent -> Connection -> Maybe Contact -> m ()
processDirectMessage agentMsg conn = \ case
Nothing -> case agentMsg of
2021-08-05 08:38:39 +01:00
REQ confId connInfo -> do
2021-07-24 18:11:04 +01:00
saveConnInfo conn connInfo
acceptAgentConnection conn confId $ XInfo profile
INFO connInfo ->
saveConnInfo conn connInfo
2021-09-04 07:32:56 +01:00
MSG meta _ ->
withAckMessage agentConnId meta $ pure ()
2021-08-14 21:04:51 +01:00
_ -> pure ()
2021-07-24 18:11:04 +01:00
Just ct @ Contact { localDisplayName = c } -> case agentMsg of
2021-09-04 07:32:56 +01:00
MSG meta msgBody -> withAckMessage agentConnId meta $ do
2021-07-11 12:22:22 +01:00
ChatMessage { chatMsgEvent } <- liftEither $ parseChatMessage msgBody
2021-07-04 18:42:24 +01:00
case chatMsgEvent of
2021-07-24 10:26:28 +01:00
XMsgNew ( MsgContent MTText [] body ) -> newTextMessage c meta $ find ( isSimplexContentType XCText ) body
2021-09-05 14:08:29 +01:00
XFile fInv -> processFileInvitation ct meta fInv
2021-08-22 15:56:36 +01:00
XInfo p -> xInfo ct p
2021-07-24 10:26:28 +01:00
XGrpInv gInv -> processGroupInvitation ct gInv
2021-07-27 08:08:05 +01:00
XInfoProbe probe -> xInfoProbe ct probe
XInfoProbeCheck probeHash -> xInfoProbeCheck ct probeHash
XInfoProbeOk probe -> xInfoProbeOk ct probe
2021-07-04 18:42:24 +01:00
_ -> pure ()
2021-08-05 08:38:39 +01:00
REQ confId connInfo -> do
2021-07-24 10:26:28 +01:00
-- confirming direct connection with a member
ChatMessage { chatMsgEvent } <- liftEither $ parseChatMessage connInfo
case chatMsgEvent of
XGrpMemInfo _memId _memProfile -> do
-- TODO check member ID
-- TODO update member profile
2021-07-24 18:11:04 +01:00
acceptAgentConnection conn confId XOk
2021-08-05 08:38:39 +01:00
_ -> messageError " REQ from member must have x.grp.mem.info "
2021-07-24 10:26:28 +01:00
INFO connInfo -> do
ChatMessage { chatMsgEvent } <- liftEither $ parseChatMessage connInfo
case chatMsgEvent of
XGrpMemInfo _memId _memProfile -> do
-- TODO check member ID
-- TODO update member profile
pure ()
XOk -> pure ()
_ -> messageError " INFO from member must have x.grp.mem.info or x.ok "
2021-07-24 18:11:04 +01:00
CON ->
2021-07-24 10:26:28 +01:00
withStore ( \ st -> getViaGroupMember st user ct ) >>= \ case
Nothing -> do
showContactConnected ct
setActive $ ActiveC c
showToast ( c <> " > " ) " connected "
Just ( gName , m ) ->
2021-07-27 08:08:05 +01:00
when ( memberIsReady m ) $ do
notifyMemberConnected gName m
when ( memberCategory m == GCPreMember ) $ probeMatchingContacts ct
2021-07-06 19:07:03 +01:00
END -> do
2021-08-14 21:04:51 +01:00
showContactAnotherClient c
showToast ( c <> " > " ) " connected to another client "
unsetActive $ ActiveC c
DOWN -> do
2021-07-06 19:07:03 +01:00
showContactDisconnected c
2021-07-24 10:26:28 +01:00
showToast ( c <> " > " ) " disconnected "
2021-08-14 21:04:51 +01:00
UP -> do
showContactSubscribed c
showToast ( c <> " > " ) " is active "
setActive $ ActiveC c
_ -> pure ()
2021-07-24 10:26:28 +01:00
2021-07-24 18:11:04 +01:00
processGroupMessage :: ACommand 'Agent -> Connection -> GroupName -> GroupMember -> m ()
processGroupMessage agentMsg conn gName m = case agentMsg of
2021-08-05 08:38:39 +01:00
REQ confId connInfo -> do
2021-07-24 18:11:04 +01:00
ChatMessage { chatMsgEvent } <- liftEither $ parseChatMessage connInfo
case memberCategory m of
GCInviteeMember ->
case chatMsgEvent of
XGrpAcpt memId
| memId == memberId m -> do
2021-08-02 20:10:24 +01:00
withStore $ \ st -> updateGroupMemberStatus st userId m GSMemAccepted
2021-07-24 18:11:04 +01:00
acceptAgentConnection conn confId XOk
| otherwise -> messageError " x.grp.acpt: memberId is different from expected "
2021-08-05 08:38:39 +01:00
_ -> messageError " REQ from invited member must have x.grp.acpt "
2021-07-24 18:11:04 +01:00
_ ->
case chatMsgEvent of
XGrpMemInfo memId _memProfile
| memId == memberId m -> do
-- TODO update member profile
Group { membership } <- withStore $ \ st -> getGroup st user gName
acceptAgentConnection conn confId $ XGrpMemInfo ( memberId membership ) profile
| otherwise -> messageError " x.grp.mem.info: memberId is different from expected "
2021-08-05 08:38:39 +01:00
_ -> messageError " REQ from member must have x.grp.mem.info "
2021-07-24 18:11:04 +01:00
INFO connInfo -> do
ChatMessage { chatMsgEvent } <- liftEither $ parseChatMessage connInfo
case chatMsgEvent of
XGrpMemInfo memId _memProfile
| memId == memberId m -> do
-- TODO update member profile
pure ()
| otherwise -> messageError " x.grp.mem.info: memberId is different from expected "
XOk -> pure ()
_ -> messageError " INFO from member must have x.grp.mem.info "
pure ()
CON -> do
group @ Group { members , membership } <- withStore $ \ st -> getGroup st user gName
withStore $ \ st -> do
2021-08-02 20:10:24 +01:00
updateGroupMemberStatus st userId m GSMemConnected
2021-07-27 08:08:05 +01:00
unless ( memberActive membership ) $
2021-08-02 20:10:24 +01:00
updateGroupMemberStatus st userId membership GSMemConnected
2021-07-24 18:11:04 +01:00
-- TODO forward any pending (GMIntroInvReceived) introductions
case memberCategory m of
GCHostMember -> do
showUserJoinedGroup gName
setActive $ ActiveG gName
showToast ( " # " <> gName ) " you are connected to group "
GCInviteeMember -> do
showJoinedGroupMember gName m
setActive $ ActiveG gName
showToast ( " # " <> gName ) $ " member " <> localDisplayName ( m :: GroupMember ) <> " is connected "
intros <- withStore $ \ st -> createIntroductions st group m
sendGroupMessage members . XGrpMemNew $ memberInfo m
forM_ intros $ \ intro -> do
sendDirectMessage agentConnId . XGrpMemIntro . memberInfo $ reMember intro
withStore $ \ st -> updateIntroStatus st intro GMIntroSent
_ -> do
-- TODO send probe and decide whether to use existing contact connection or the new contact connection
-- TODO notify member who forwarded introduction - question - where it is stored? There is via_contact but probably there should be via_member in group_members table
withStore ( \ st -> getViaGroupContact st user m ) >>= \ case
Nothing -> do
notifyMemberConnected gName m
messageError " implementation error: connected member does not have contact "
Just ct ->
2021-07-27 08:08:05 +01:00
when ( contactIsReady ct ) $ do
notifyMemberConnected gName m
when ( memberCategory m == GCPreMember ) $ probeMatchingContacts ct
2021-09-04 07:32:56 +01:00
MSG meta msgBody -> withAckMessage agentConnId meta $ do
2021-07-24 18:11:04 +01:00
ChatMessage { chatMsgEvent } <- liftEither $ parseChatMessage msgBody
case chatMsgEvent of
XMsgNew ( MsgContent MTText [] body ) ->
newGroupTextMessage gName m meta $ find ( isSimplexContentType XCText ) body
2021-09-05 14:08:29 +01:00
XFile fInv -> processGroupFileInvitation gName m meta fInv
2021-07-24 18:11:04 +01:00
XGrpMemNew memInfo -> xGrpMemNew gName m memInfo
XGrpMemIntro memInfo -> xGrpMemIntro gName m memInfo
XGrpMemInv memId introInv -> xGrpMemInv gName m memId introInv
XGrpMemFwd memInfo introInv -> xGrpMemFwd gName m memInfo introInv
2021-08-02 20:10:24 +01:00
XGrpMemDel memId -> xGrpMemDel gName m memId
XGrpLeave -> xGrpLeave gName m
XGrpDel -> xGrpDel gName m
2021-07-24 18:11:04 +01:00
_ -> messageError $ " unsupported message: " <> T . pack ( show chatMsgEvent )
2021-08-14 21:04:51 +01:00
_ -> pure ()
2021-07-24 10:26:28 +01:00
2021-09-04 07:32:56 +01:00
processSndFileConn :: ACommand 'Agent -> Connection -> SndFileTransfer -> m ()
processSndFileConn agentMsg conn ft @ SndFileTransfer { fileId , fileName , fileStatus } =
case agentMsg of
REQ confId connInfo -> do
ChatMessage { chatMsgEvent } <- liftEither $ parseChatMessage connInfo
case chatMsgEvent of
XFileAcpt name
| name == fileName -> do
withStore $ \ st -> updateSndFileStatus st ft FSAccepted
acceptAgentConnection conn confId XOk
| otherwise -> messageError " x.file.acpt: fileName is different from expected "
_ -> messageError " REQ from file connection must have x.file.acpt "
CON -> do
withStore $ \ st -> updateSndFileStatus st ft FSConnected
2021-09-05 14:08:29 +01:00
showSndFileStart ft
2021-09-04 07:32:56 +01:00
sendFileChunk ft
SENT msgId -> do
withStore $ \ st -> updateSndFileChunkSent st ft msgId
unless ( fileStatus == FSCancelled ) $ sendFileChunk ft
MERR _ err -> do
cancelSndFileTransfer ft
case err of
2021-09-05 14:08:29 +01:00
SMP SMP . AUTH -> unless ( fileStatus == FSCancelled ) $ showSndFileRcvCancelled ft
2021-09-04 07:32:56 +01:00
_ -> chatError $ CEFileSend fileId err
MSG meta _ ->
withAckMessage agentConnId meta $ pure ()
_ -> pure ()
processRcvFileConn :: ACommand 'Agent -> Connection -> RcvFileTransfer -> m ()
processRcvFileConn agentMsg _conn ft @ RcvFileTransfer { fileId , chunkSize } =
case agentMsg of
CON -> do
withStore $ \ st -> updateRcvFileStatus st ft FSConnected
2021-09-05 14:08:29 +01:00
showRcvFileStart ft
2021-09-04 07:32:56 +01:00
MSG meta @ MsgMeta { recipient = ( msgId , _ ) , integrity } msgBody -> withAckMessage agentConnId meta $ do
parseFileChunk msgBody >>= \ case
( 0 , _ ) -> do
cancelRcvFileTransfer ft
2021-09-05 14:08:29 +01:00
showRcvFileSndCancelled ft
2021-09-04 07:32:56 +01:00
( chunkNo , chunk ) -> do
case integrity of
MsgOk -> pure ()
MsgError MsgDuplicate -> pure () -- TODO remove once agent removes duplicates
MsgError e ->
badRcvFileChunk ft $ " invalid file chunk number " <> show chunkNo <> " : " <> show e
withStore ( \ st -> createRcvFileChunk st ft chunkNo msgId ) >>= \ case
RcvChunkOk ->
if B . length chunk /= fromInteger chunkSize
then badRcvFileChunk ft " incorrect chunk size "
else appendFileChunk ft chunkNo chunk
RcvChunkFinal ->
if B . length chunk > fromInteger chunkSize
then badRcvFileChunk ft " incorrect chunk size "
else do
appendFileChunk ft chunkNo chunk
2021-09-05 14:08:29 +01:00
withStore $ \ st -> do
updateRcvFileStatus st ft FSComplete
deleteRcvFileChunks st ft
showRcvFileComplete ft
2021-09-04 07:32:56 +01:00
closeFileHandle fileId rcvFiles
withAgent ( ` deleteConnection ` agentConnId )
RcvChunkDuplicate -> pure ()
RcvChunkError -> badRcvFileChunk ft $ " incorrect chunk number " <> show chunkNo
_ -> pure ()
withAckMessage :: ConnId -> MsgMeta -> m () -> m ()
withAckMessage cId MsgMeta { recipient = ( msgId , _ ) } action =
action ` E . finally ` withAgent ( \ a -> ackMessage a cId msgId ` catchError ` \ _ -> pure () )
badRcvFileChunk :: RcvFileTransfer -> String -> m ()
badRcvFileChunk ft @ RcvFileTransfer { fileStatus } err =
case fileStatus of
RFSCancelled _ -> pure ()
_ -> do
cancelRcvFileTransfer ft
chatError $ CEFileRcvChunk err
2021-07-24 10:26:28 +01:00
notifyMemberConnected :: GroupName -> GroupMember -> m ()
notifyMemberConnected gName m @ GroupMember { localDisplayName } = do
showConnectedToGroupMember gName m
setActive $ ActiveG gName
showToast ( " # " <> gName ) $ " member " <> localDisplayName <> " is connected "
2021-07-27 08:08:05 +01:00
probeMatchingContacts :: Contact -> m ()
probeMatchingContacts ct = do
gVar <- asks idsDrg
( probe , probeId ) <- withStore $ \ st -> createSentProbe st gVar userId ct
sendDirectMessage ( contactConnId ct ) $ XInfoProbe probe
cs <- withStore ( \ st -> getMatchingContacts st userId ct )
let probeHash = C . sha256Hash probe
forM_ cs $ \ c -> sendProbeHash c probeHash probeId ` catchError ` const ( pure () )
where
sendProbeHash c probeHash probeId = do
sendDirectMessage ( contactConnId c ) $ XInfoProbeCheck probeHash
withStore $ \ st -> createSentProbeHash st userId probeId c
2021-07-24 10:26:28 +01:00
messageWarning :: Text -> m ()
2021-09-04 07:32:56 +01:00
messageWarning = showMessageError " warning "
2021-07-24 10:26:28 +01:00
messageError :: Text -> m ()
2021-09-04 07:32:56 +01:00
messageError = showMessageError " error "
2021-07-24 10:26:28 +01:00
newTextMessage :: ContactName -> MsgMeta -> Maybe MsgContentBody -> m ()
2021-07-07 22:46:38 +01:00
newTextMessage c meta = \ case
2021-07-24 10:26:28 +01:00
Just MsgContentBody { contentData = bs } -> do
2021-07-07 22:46:38 +01:00
let text = safeDecodeUtf8 bs
2021-09-05 14:08:29 +01:00
showReceivedMessage c ( snd $ broker meta ) ( msgPlain text ) ( integrity meta )
2021-07-24 10:26:28 +01:00
showToast ( c <> " > " ) text
2021-07-07 22:46:38 +01:00
setActive $ ActiveC c
2021-07-24 10:26:28 +01:00
_ -> messageError " x.msg.new: no expected message body "
newGroupTextMessage :: GroupName -> GroupMember -> MsgMeta -> Maybe MsgContentBody -> m ()
newGroupTextMessage gName GroupMember { localDisplayName = c } meta = \ case
Just MsgContentBody { contentData = bs } -> do
let text = safeDecodeUtf8 bs
2021-09-05 14:08:29 +01:00
showReceivedGroupMessage gName c ( snd $ broker meta ) ( msgPlain text ) ( integrity meta )
2021-07-24 10:26:28 +01:00
showToast ( " # " <> gName <> " " <> c <> " > " ) text
setActive $ ActiveG gName
_ -> messageError " x.msg.new: no expected message body "
2021-07-07 22:46:38 +01:00
2021-09-05 14:08:29 +01:00
processFileInvitation :: Contact -> MsgMeta -> FileInvitation -> m ()
processFileInvitation contact @ Contact { localDisplayName = c } meta fInv = do
2021-09-04 07:32:56 +01:00
-- TODO chunk size has to be sent as part of invitation
chSize <- asks $ fileChunkSize . config
2021-09-05 14:08:29 +01:00
ft <- withStore $ \ st -> createRcvFileTransfer st userId contact fInv chSize
showReceivedMessage c ( snd $ broker meta ) ( receivedFileInvitation ft ) ( integrity meta )
2021-09-04 07:32:56 +01:00
setActive $ ActiveC c
2021-09-05 14:08:29 +01:00
processGroupFileInvitation :: GroupName -> GroupMember -> MsgMeta -> FileInvitation -> m ()
processGroupFileInvitation gName m @ GroupMember { localDisplayName = c } meta fInv = do
chSize <- asks $ fileChunkSize . config
ft <- withStore $ \ st -> createRcvGroupFileTransfer st userId m fInv chSize
showReceivedGroupMessage gName c ( snd $ broker meta ) ( receivedFileInvitation ft ) ( integrity meta )
setActive $ ActiveG gName
2021-07-24 10:26:28 +01:00
processGroupInvitation :: Contact -> GroupInvitation -> m ()
processGroupInvitation ct @ Contact { localDisplayName } inv @ ( GroupInvitation ( fromMemId , fromRole ) ( memId , memRole ) _ _ ) = do
2021-08-02 20:10:24 +01:00
when ( fromRole < GRAdmin || fromRole < memRole ) $ chatError ( CEGroupContactRole localDisplayName )
when ( fromMemId == memId ) $ chatError CEGroupDuplicateMemberId
2021-07-16 07:40:55 +01:00
group <- withStore $ \ st -> createGroupInvitation st user ct inv
2021-07-24 10:26:28 +01:00
showReceivedGroupInvitation group localDisplayName memRole
2021-07-12 19:00:03 +01:00
2021-08-22 15:56:36 +01:00
xInfo :: Contact -> Profile -> m ()
xInfo c @ Contact { profile = p } p' = unless ( p == p' ) $ do
c' <- withStore $ \ st -> updateContactProfile st userId c p'
showContactUpdated c c'
2021-07-27 08:08:05 +01:00
xInfoProbe :: Contact -> ByteString -> m ()
xInfoProbe c2 probe = do
r <- withStore $ \ st -> matchReceivedProbe st userId c2 probe
forM_ r $ \ c1 -> probeMatch c1 c2 probe
xInfoProbeCheck :: Contact -> ByteString -> m ()
xInfoProbeCheck c1 probeHash = do
r <- withStore $ \ st -> matchReceivedProbeHash st userId c1 probeHash
2021-08-02 20:10:24 +01:00
forM_ r . uncurry $ probeMatch c1
2021-07-27 08:08:05 +01:00
probeMatch :: Contact -> Contact -> ByteString -> m ()
probeMatch c1 @ Contact { profile = p1 } c2 @ Contact { profile = p2 } probe =
when ( p1 == p2 ) $ do
sendDirectMessage ( contactConnId c1 ) $ XInfoProbeOk probe
mergeContacts c1 c2
xInfoProbeOk :: Contact -> ByteString -> m ()
xInfoProbeOk c1 probe = do
r <- withStore $ \ st -> matchSentProbe st userId c1 probe
forM_ r $ \ c2 -> mergeContacts c1 c2
mergeContacts :: Contact -> Contact -> m ()
mergeContacts to from = do
withStore $ \ st -> mergeContactRecords st userId to from
showContactsMerged to from
2021-07-06 19:07:03 +01:00
parseChatMessage :: ByteString -> Either ChatError ChatMessage
parseChatMessage msgBody = first ChatErrorMessage ( parseAll rawChatMessageP msgBody >>= toChatMessage )
saveConnInfo :: Connection -> ConnInfo -> m ()
saveConnInfo activeConn connInfo = do
2021-07-11 12:22:22 +01:00
ChatMessage { chatMsgEvent } <- liftEither $ parseChatMessage connInfo
2021-07-06 19:07:03 +01:00
case chatMsgEvent of
2021-07-11 12:22:22 +01:00
XInfo p ->
withStore $ \ st -> createDirectContact st userId activeConn p
2021-07-24 18:11:04 +01:00
-- TODO show/log error, other events in SMP confirmation
_ -> pure ()
xGrpMemNew :: GroupName -> GroupMember -> MemberInfo -> m ()
xGrpMemNew gName m memInfo @ ( MemberInfo memId _ _ ) = do
group @ Group { membership } <- withStore $ \ st -> getGroup st user gName
when ( memberId membership /= memId ) $
if isMember memId group
then messageError " x.grp.mem.new error: member already exists "
else do
newMember <- withStore $ \ st -> createNewGroupMember st user group memInfo GCPostMember GSMemAnnounced
showJoinedGroupMemberConnecting gName m newMember
xGrpMemIntro :: GroupName -> GroupMember -> MemberInfo -> m ()
xGrpMemIntro gName m memInfo @ ( MemberInfo memId _ _ ) =
case memberCategory m of
GCHostMember -> do
group <- withStore $ \ st -> getGroup st user gName
if isMember memId group
then messageWarning " x.grp.mem.intro ignored: member already exists "
else do
2021-12-02 11:17:09 +00:00
( groupConnId , groupConnReq ) <- withAgent createConnection
( directConnId , directConnReq ) <- withAgent createConnection
2021-07-24 18:11:04 +01:00
newMember <- withStore $ \ st -> createIntroReMember st user group m memInfo groupConnId directConnId
2021-12-02 11:17:09 +00:00
let msg = XGrpMemInv memId IntroInvitation { groupConnReq , directConnReq }
2021-07-24 18:11:04 +01:00
sendDirectMessage agentConnId msg
2021-08-02 20:10:24 +01:00
withStore $ \ st -> updateGroupMemberStatus st userId newMember GSMemIntroInvited
2021-07-24 18:11:04 +01:00
_ -> messageError " x.grp.mem.intro can be only sent by host member "
xGrpMemInv :: GroupName -> GroupMember -> MemberId -> IntroInvitation -> m ()
xGrpMemInv gName m memId introInv =
case memberCategory m of
GCInviteeMember -> do
group <- withStore $ \ st -> getGroup st user gName
case find ( ( == memId ) . memberId ) $ members group of
Nothing -> messageError " x.grp.mem.inv error: referenced member does not exists "
Just reMember -> do
intro <- withStore $ \ st -> saveIntroInvitation st reMember m introInv
case activeConn ( reMember :: GroupMember ) of
Nothing -> pure () -- this is not an error, introduction will be forwarded once the member is connected
Just Connection { agentConnId = reAgentConnId } -> do
sendDirectMessage reAgentConnId $ XGrpMemFwd ( memberInfo m ) introInv
withStore $ \ st -> updateIntroStatus st intro GMIntroInvForwarded
_ -> messageError " x.grp.mem.inv can be only sent by invitee member "
xGrpMemFwd :: GroupName -> GroupMember -> MemberInfo -> IntroInvitation -> m ()
2021-12-02 11:17:09 +00:00
xGrpMemFwd gName m memInfo @ ( MemberInfo memId _ _ ) introInv @ IntroInvitation { groupConnReq , directConnReq } = do
2021-07-24 18:11:04 +01:00
group @ Group { membership } <- withStore $ \ st -> getGroup st user gName
toMember <- case find ( ( == memId ) . memberId ) $ members group of
-- TODO if the missed messages are correctly sent as soon as there is connection before anything else is sent
-- the situation when member does not exist is an error
-- member receiving x.grp.mem.fwd should have also received x.grp.mem.new prior to that.
-- For now, this branch compensates for the lack of delayed message delivery.
Nothing -> withStore $ \ st -> createNewGroupMember st user group memInfo GCPostMember GSMemAnnounced
Just m' -> pure m'
withStore $ \ st -> saveMemberInvitation st toMember introInv
let msg = XGrpMemInfo ( memberId membership ) profile
2021-12-02 11:17:09 +00:00
groupConnId <- withAgent $ \ a -> joinConnection a groupConnReq $ directMessage msg
directConnId <- withAgent $ \ a -> joinConnection a directConnReq $ directMessage msg
2021-07-24 18:11:04 +01:00
withStore $ \ st -> createIntroToMemberContact st userId m toMember groupConnId directConnId
2021-07-06 19:07:03 +01:00
2021-08-02 20:10:24 +01:00
xGrpMemDel :: GroupName -> GroupMember -> MemberId -> m ()
xGrpMemDel gName m memId = do
Group { membership , members } <- withStore $ \ st -> getGroup st user gName
if memberId membership == memId
then do
mapM_ deleteMemberConnection members
withStore $ \ st -> updateGroupMemberStatus st userId membership GSMemRemoved
showDeletedMemberUser gName m
else case find ( ( == memId ) . memberId ) members of
Nothing -> messageError " x.grp.mem.del with unknown member ID "
Just member -> do
let mRole = memberRole m
if mRole < GRAdmin || mRole < memberRole member
then messageError " x.grp.mem.del with insufficient member permissions "
else do
deleteMemberConnection member
withStore $ \ st -> updateGroupMemberStatus st userId member GSMemRemoved
showDeletedMember gName ( Just m ) ( Just member )
xGrpLeave :: GroupName -> GroupMember -> m ()
xGrpLeave gName m = do
deleteMemberConnection m
withStore $ \ st -> updateGroupMemberStatus st userId m GSMemLeft
showLeftMember gName m
xGrpDel :: GroupName -> GroupMember -> m ()
xGrpDel gName m = do
when ( memberRole m /= GROwner ) $ chatError CEGroupUserRole
ms <- withStore $ \ st -> do
Group { members , membership } <- getGroup st user gName
updateGroupMemberStatus st userId membership GSMemGroupDeleted
pure members
mapM_ deleteMemberConnection ms
showGroupDeleted gName m
2021-09-04 07:32:56 +01:00
sendFileChunk :: ChatMonad m => SndFileTransfer -> m ()
sendFileChunk ft @ SndFileTransfer { fileId , fileStatus , agentConnId } =
unless ( fileStatus == FSComplete || fileStatus == FSCancelled ) $
withStore ( ` createSndFileChunk ` ft ) >>= \ case
Just chunkNo -> sendFileChunkNo ft chunkNo
Nothing -> do
2021-09-05 14:08:29 +01:00
withStore $ \ st -> do
updateSndFileStatus st ft FSComplete
deleteSndFileChunks st ft
showSndFileComplete ft
2021-09-04 07:32:56 +01:00
closeFileHandle fileId sndFiles
withAgent ( ` deleteConnection ` agentConnId )
sendFileChunkNo :: ChatMonad m => SndFileTransfer -> Integer -> m ()
sendFileChunkNo ft @ SndFileTransfer { agentConnId } chunkNo = do
bytes <- readFileChunk ft chunkNo
msgId <- withAgent $ \ a -> sendMessage a agentConnId $ serializeFileChunk chunkNo bytes
withStore $ \ st -> updateSndFileChunkMsg st ft chunkNo msgId
readFileChunk :: ChatMonad m => SndFileTransfer -> Integer -> m ByteString
readFileChunk SndFileTransfer { fileId , filePath , chunkSize } chunkNo =
read_ ` E . catch ` ( chatError . CEFileRead filePath )
where
read_ = do
h <- getFileHandle fileId filePath sndFiles ReadMode
pos <- hTell h
let pos' = ( chunkNo - 1 ) * chunkSize
when ( pos /= pos' ) $ hSeek h AbsoluteSeek pos'
liftIO . B . hGet h $ fromInteger chunkSize
parseFileChunk :: ChatMonad m => ByteString -> m ( Integer , ByteString )
parseFileChunk msg =
liftEither . first ( ChatError . CEFileRcvChunk ) $
parseAll ( ( , ) <$> A . decimal <* A . space <*> A . takeByteString ) msg
serializeFileChunk :: Integer -> ByteString -> ByteString
serializeFileChunk chunkNo bytes = bshow chunkNo <> " " <> bytes
appendFileChunk :: ChatMonad m => RcvFileTransfer -> Integer -> ByteString -> m ()
appendFileChunk ft @ RcvFileTransfer { fileId , fileStatus } chunkNo chunk =
case fileStatus of
RFSConnected RcvFileInfo { filePath } -> append_ filePath
RFSCancelled _ -> pure ()
_ -> chatError $ CEFileInternal " receiving file transfer not in progress "
where
append_ fPath = do
h <- getFileHandle fileId fPath rcvFiles AppendMode
E . try ( liftIO $ B . hPut h chunk >> hFlush h ) >>= \ case
Left e -> chatError $ CEFileWrite fPath e
Right () -> withStore $ \ st -> updatedRcvFileChunkStored st ft chunkNo
getFileHandle :: ChatMonad m => Int64 -> FilePath -> ( ChatController -> TVar ( Map Int64 Handle ) ) -> IOMode -> m Handle
getFileHandle fileId filePath files ioMode = do
fs <- asks files
h_ <- M . lookup fileId <$> readTVarIO fs
maybe ( newHandle fs ) pure h_
where
newHandle fs = do
-- TODO handle errors
h <- liftIO ( openFile filePath ioMode )
atomically . modifyTVar fs $ M . insert fileId h
pure h
isFileActive :: ChatMonad m => Int64 -> ( ChatController -> TVar ( Map Int64 Handle ) ) -> m Bool
isFileActive fileId files = do
fs <- asks files
isJust . M . lookup fileId <$> readTVarIO fs
cancelRcvFileTransfer :: ChatMonad m => RcvFileTransfer -> m ()
cancelRcvFileTransfer ft @ RcvFileTransfer { fileId , fileStatus } = do
closeFileHandle fileId rcvFiles
2021-09-05 14:08:29 +01:00
withStore $ \ st -> do
updateRcvFileStatus st ft FSCancelled
deleteRcvFileChunks st ft
2021-09-04 07:32:56 +01:00
case fileStatus of
RFSAccepted RcvFileInfo { agentConnId } -> withAgent ( ` suspendConnection ` agentConnId )
RFSConnected RcvFileInfo { agentConnId } -> withAgent ( ` suspendConnection ` agentConnId )
_ -> pure ()
cancelSndFileTransfer :: ChatMonad m => SndFileTransfer -> m ()
cancelSndFileTransfer ft @ SndFileTransfer { agentConnId , fileStatus } =
unless ( fileStatus == FSCancelled || fileStatus == FSComplete ) $ do
2021-09-05 14:08:29 +01:00
withStore $ \ st -> do
updateSndFileStatus st ft FSCancelled
deleteSndFileChunks st ft
2021-09-04 07:32:56 +01:00
withAgent $ \ a -> do
2021-09-05 14:08:29 +01:00
void ( sendMessage a agentConnId " 0 " ) ` catchError ` \ _ -> pure ()
2021-09-04 07:32:56 +01:00
suspendConnection a agentConnId
closeFileHandle :: ChatMonad m => Int64 -> ( ChatController -> TVar ( Map Int64 Handle ) ) -> m ()
closeFileHandle fileId files = do
fs <- asks files
h_ <- atomically . stateTVar fs $ \ m -> ( M . lookup fileId m , M . delete fileId m )
mapM_ hClose h_ ` E . catch ` \ ( _ :: E . SomeException ) -> pure ()
chatError :: ChatMonad m => ChatErrorType -> m a
2021-08-02 20:10:24 +01:00
chatError = throwError . ChatError
deleteMemberConnection :: ChatMonad m => GroupMember -> m ()
2021-08-14 21:04:51 +01:00
deleteMemberConnection m @ GroupMember { activeConn } = do
-- User {userId} <- asks currentUser
withAgent $ forM_ ( memberConnId m ) . suspendConnection
-- withStore $ \st -> deleteGroupMemberConnection st userId m
forM_ activeConn $ \ conn -> withStore $ \ st -> updateConnectionStatus st conn ConnDeleted
2021-08-02 20:10:24 +01:00
2021-07-16 07:40:55 +01:00
sendDirectMessage :: ChatMonad m => ConnId -> ChatMsgEvent -> m ()
sendDirectMessage agentConnId chatMsgEvent =
void . withAgent $ \ a -> sendMessage a agentConnId $ directMessage chatMsgEvent
directMessage :: ChatMsgEvent -> ByteString
directMessage chatMsgEvent =
serializeRawChatMessage $
rawChatMessage ChatMessage { chatMsgId = Nothing , chatMsgEvent , chatDAG = Nothing }
2021-07-24 10:26:28 +01:00
sendGroupMessage :: ChatMonad m => [ GroupMember ] -> ChatMsgEvent -> m ()
sendGroupMessage members chatMsgEvent = do
let msg = directMessage chatMsgEvent
-- TODO once scheduled delivery is implemented memberActive should be changed to memberCurrent
withAgent $ \ a ->
forM_ ( filter memberActive members ) $
traverse ( \ connId -> sendMessage a connId msg ) . memberConnId
acceptAgentConnection :: ChatMonad m => Connection -> ConfirmationId -> ChatMsgEvent -> m ()
acceptAgentConnection conn @ Connection { agentConnId } confId msg = do
2021-08-05 08:38:39 +01:00
withAgent $ \ a -> acceptConnection a agentConnId confId $ directMessage msg
2021-07-24 10:26:28 +01:00
withStore $ \ st -> updateConnectionStatus st conn ConnAccepted
2021-07-05 19:54:44 +01:00
getCreateActiveUser :: SQLiteStore -> IO User
getCreateActiveUser st = do
user <-
getUsers st >>= \ case
[] -> newUser
users -> maybe ( selectUser users ) pure ( find activeUser users )
putStrLn $ " Current user: " <> userStr user
pure user
where
newUser :: IO User
newUser = do
putStrLn
" No user profiles found, it will be created now. \ n \
2021-07-14 20:11:41 +01:00
\ Please choose your display name and your full name .\ n \
2021-07-05 19:54:44 +01:00
\ They will be sent to your contacts when you connect .\ n \
\ They are only stored on your device and you can change them later . "
loop
where
loop = do
2021-07-14 20:11:41 +01:00
displayName <- getContactName
fullName <- T . pack <$> getWithPrompt " full name (optional) "
liftIO ( runExceptT $ createUser st Profile { displayName , fullName } True ) >>= \ case
Left SEDuplicateName -> do
putStrLn " chosen display name is already used by another profile on this device, choose another one "
2021-07-05 19:54:44 +01:00
loop
Left e -> putStrLn ( " database error " <> show e ) >> exitFailure
Right user -> pure user
selectUser :: [ User ] -> IO User
selectUser [ user ] = do
liftIO $ setActiveUser st ( userId user )
pure user
selectUser users = do
2021-07-05 20:05:07 +01:00
putStrLn " Select user profile: "
2021-07-05 19:54:44 +01:00
forM_ ( zip [ 1 .. ] users ) $ \ ( n :: Int , user ) -> putStrLn $ show n <> " - " <> userStr user
loop
where
loop = do
nStr <- getWithPrompt $ " user profile number (1 .. " <> show ( length users ) <> " ) "
case readMaybe nStr :: Maybe Int of
Nothing -> putStrLn " invalid user number " >> loop
Just n
| n <= 0 || n > length users -> putStrLn " invalid user number " >> loop
| otherwise -> do
let user = users !! ( n - 1 )
liftIO $ setActiveUser st ( userId user )
pure user
userStr :: User -> String
2021-07-14 20:11:41 +01:00
userStr User { localDisplayName , profile = Profile { fullName } } =
2021-08-22 15:56:36 +01:00
T . unpack $ localDisplayName <> if T . null fullName || localDisplayName == fullName then " " else " ( " <> fullName <> " ) "
2021-07-14 20:11:41 +01:00
getContactName :: IO ContactName
getContactName = do
displayName <- getWithPrompt " display name (no spaces) "
if null displayName || isJust ( find ( == ' ' ) displayName )
then putStrLn " display name has space(s), choose another one " >> getContactName
else pure $ T . pack displayName
2021-07-05 19:54:44 +01:00
getWithPrompt :: String -> IO String
getWithPrompt s = putStr ( s <> " : " ) >> hFlush stdout >> getLine
2021-06-25 18:18:24 +01:00
2021-07-04 18:42:24 +01:00
showToast :: ( MonadUnliftIO m , MonadReader ChatController m ) => Text -> Text -> m ()
showToast title text = atomically . ( ` writeTBQueue ` Notification { title , text } ) =<< asks notifyQ
2021-06-26 20:20:33 +01:00
notificationSubscriber :: ( MonadUnliftIO m , MonadReader ChatController m ) => m ()
notificationSubscriber = do
ChatController { notifyQ , sendNotification } <- ask
forever $ atomically ( readTBQueue notifyQ ) >>= liftIO . sendNotification
2021-07-05 19:54:44 +01:00
withAgent :: ChatMonad m => ( AgentClient -> ExceptT AgentErrorType m a ) -> m a
withAgent action =
2021-06-25 18:18:24 +01:00
asks smpAgent
>>= runExceptT . action
2021-07-05 19:54:44 +01:00
>>= liftEither . first ChatErrorAgent
2021-06-25 18:18:24 +01:00
2021-07-04 18:42:24 +01:00
withStore ::
ChatMonad m =>
( forall m' . ( MonadUnliftIO m' , MonadError StoreError m' ) => SQLiteStore -> m' a ) ->
m a
2021-07-12 19:00:03 +01:00
withStore action =
asks chatStore
>>= runExceptT . action
>>= liftEither . first ChatErrorStore
2021-07-04 18:42:24 +01:00
2021-06-25 18:18:24 +01:00
chatCommandP :: Parser ChatCommand
chatCommandP =
2021-09-05 05:38:11 +10:00
( " /help files " <|> " /help file " <|> " /hf " ) $> FilesHelp
<|> ( " /help groups " <|> " /help group " <|> " /hg " ) $> GroupsHelp
<|> ( " /help " <|> " /h " ) $> ChatHelp
2021-07-24 10:26:28 +01:00
<|> ( " /group # " <|> " /group " <|> " /g # " <|> " /g " ) *> ( NewGroup <$> groupProfile )
<|> ( " /add # " <|> " /add " <|> " /a # " <|> " /a " ) *> ( AddMember <$> displayName <* A . space <*> displayName <*> memberRole )
<|> ( " /join # " <|> " /join " <|> " /j # " <|> " /j " ) *> ( JoinGroup <$> displayName )
2021-08-02 20:10:24 +01:00
<|> ( " /remove # " <|> " /remove " <|> " /rm # " <|> " /rm " ) *> ( RemoveMember <$> displayName <* A . space <*> displayName )
<|> ( " /leave # " <|> " /leave " <|> " /l # " <|> " /l " ) *> ( LeaveGroup <$> displayName )
2021-07-14 20:11:41 +01:00
<|> ( " /delete # " <|> " /d # " ) *> ( DeleteGroup <$> displayName )
2021-07-27 08:08:05 +01:00
<|> ( " /members # " <|> " /members " <|> " /ms # " <|> " /ms " ) *> ( ListMembers <$> displayName )
2021-07-14 20:11:41 +01:00
<|> A . char '#' *> ( SendGroupMessage <$> displayName <* A . space <*> A . takeByteString )
2021-12-02 11:17:09 +00:00
<|> ( " /connect " <|> " /c " ) *> ( Connect <$> connReqP )
2021-08-02 20:10:24 +01:00
<|> ( " /connect " <|> " /c " ) $> AddContact
2021-07-14 20:11:41 +01:00
<|> ( " /delete @ " <|> " /delete " <|> " /d @ " <|> " /d " ) *> ( DeleteContact <$> displayName )
<|> A . char '@' *> ( SendMessage <$> displayName <*> ( A . space *> A . takeByteString ) )
2021-09-04 07:32:56 +01:00
<|> ( " /file # " <|> " /f # " ) *> ( SendGroupFile <$> displayName <* A . space <*> filePath )
<|> ( " /file @ " <|> " /file " <|> " /f @ " <|> " /f " ) *> ( SendFile <$> displayName <* A . space <*> filePath )
2021-09-05 05:38:11 +10:00
<|> ( " /freceive " <|> " /fr " ) *> ( ReceiveFile <$> A . decimal <*> optional ( A . space *> filePath ) )
<|> ( " /fcancel " <|> " /fc " ) *> ( CancelFile <$> A . decimal )
<|> ( " /fstatus " <|> " /fs " ) *> ( FileStatus <$> A . decimal )
2021-06-25 18:18:24 +01:00
<|> ( " /markdown " <|> " /m " ) $> MarkdownHelp
2021-08-22 15:56:36 +01:00
<|> ( " /profile " <|> " /p " ) *> ( UpdateProfile <$> userProfile )
<|> ( " /profile " <|> " /p " ) $> ShowProfile
2021-07-27 08:08:05 +01:00
<|> ( " /quit " <|> " /q " ) $> QuitChat
2021-11-07 21:57:05 +00:00
<|> ( " /version " <|> " /v " ) $> ShowVersion
2021-06-25 18:18:24 +01:00
where
2021-07-14 20:11:41 +01:00
displayName = safeDecodeUtf8 <$> ( B . cons <$> A . satisfy refChar <*> A . takeTill ( == ' ' ) )
2021-07-11 12:22:22 +01:00
refChar c = c > ' ' && c /= '#' && c /= '@'
2021-08-22 15:56:36 +01:00
userProfile = do
cName <- displayName
fullName <- fullNameP cName
pure Profile { displayName = cName , fullName }
2021-07-12 19:00:03 +01:00
groupProfile = do
2021-07-16 07:40:55 +01:00
gName <- displayName
2021-08-22 15:56:36 +01:00
fullName <- fullNameP gName
pure GroupProfile { displayName = gName , fullName }
fullNameP name = do
n <- ( A . space *> A . takeByteString ) <|> pure " "
pure $ if B . null n then name else safeDecodeUtf8 n
2021-09-04 07:32:56 +01:00
filePath = T . unpack . safeDecodeUtf8 <$> A . takeByteString
2021-07-11 12:22:22 +01:00
memberRole =
2021-07-12 19:00:03 +01:00
( " owner " $> GROwner )
<|> ( " admin " $> GRAdmin )
2021-09-05 05:38:11 +10:00
<|> ( " member " $> GRMember )
2021-07-24 10:26:28 +01:00
<|> pure GRAdmin