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 # -}
{- # LANGUAGE ScopedTypeVariables # -}
2021-07-25 20:23:52 +01:00
{- # LANGUAGE TupleSections # -}
2022-01-11 12:41:38 +00:00
{- # LANGUAGE TypeApplications # -}
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 )
2022-03-13 19:34:03 +00:00
import qualified Data.Aeson as J
2021-07-12 19:00:03 +01:00
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 )
2022-03-10 15:45:40 +04:00
import qualified Data.ByteString.Base64 as B64
2021-06-25 18:18:24 +01:00
import Data.ByteString.Char8 ( ByteString )
2021-07-11 12:22:22 +01:00
import qualified Data.ByteString.Char8 as B
2021-12-11 12:57:12 +00:00
import Data.Char ( isSpace )
2022-05-04 13:31:00 +01:00
import Data.Fixed ( div' )
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 )
2022-03-10 15:45:40 +04:00
import Data.List.NonEmpty ( NonEmpty , nonEmpty )
import qualified Data.List.NonEmpty as L
2021-09-04 07:32:56 +01:00
import Data.Map.Strict ( Map )
import qualified Data.Map.Strict as M
2022-03-10 15:45:40 +04:00
import Data.Maybe ( fromMaybe , 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
2022-05-04 13:31:00 +01:00
import Data.Time.Clock ( UTCTime , diffUTCTime , getCurrentTime , nominalDiffTimeToSeconds )
2022-03-29 08:53:30 +01:00
import Data.Time.LocalTime ( getCurrentTimeZone , getZonedTime )
2022-01-11 12:41:38 +00:00
import Data.Word ( Word32 )
2022-05-03 10:22:35 +01:00
import Simplex.Chat.Call
2021-06-25 18:18:24 +01:00
import Simplex.Chat.Controller
2022-02-22 14:05:45 +00:00
import Simplex.Chat.Markdown
2022-01-24 16:07:17 +00:00
import Simplex.Chat.Messages
2022-03-10 15:45:40 +04:00
import Simplex.Chat.Options ( ChatOpts ( .. ) , smpServersP )
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-07-04 18:42:24 +01:00
import Simplex.Chat.Types
2022-02-06 16:18:01 +00:00
import Simplex.Chat.Util ( ifM , safeDecodeUtf8 , unlessM , whenM )
2021-06-25 18:18:24 +01:00
import Simplex.Messaging.Agent
2022-04-21 20:04:22 +01:00
import Simplex.Messaging.Agent.Env.SQLite ( AgentConfig ( .. ) , InitialAgentServers ( .. ) , 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
2022-01-11 12:41:38 +00:00
import Simplex.Messaging.Encoding
2022-01-11 08:50:44 +00:00
import Simplex.Messaging.Encoding.String
2022-04-22 13:46:05 +01:00
import Simplex.Messaging.Notifications.Client ( NtfServer )
2022-04-21 20:04:22 +01:00
import Simplex.Messaging.Notifications.Protocol ( DeviceToken ( .. ) , PushProvider ( .. ) )
2022-03-10 15:45:40 +04:00
import Simplex.Messaging.Parsers ( base64P , parseAll )
2022-02-07 15:19:34 +04:00
import Simplex.Messaging.Protocol ( ErrorType ( .. ) , MsgBody )
2021-09-04 07:32:56 +01:00
import qualified Simplex.Messaging.Protocol as SMP
2022-05-04 13:31:00 +01:00
import qualified Simplex.Messaging.TMap as TM
2022-03-13 19:34:03 +00:00
import Simplex.Messaging.Util ( 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 )
2022-02-25 16:29:36 +04:00
import UnliftIO.Async
2021-09-04 07:32:56 +01:00
import UnliftIO.Concurrent ( forkIO , threadDelay )
2022-04-15 13:16:34 +01:00
import UnliftIO.Directory
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
2021-08-02 20:10:24 +01:00
defaultChatConfig :: ChatConfig
defaultChatConfig =
ChatConfig
{ agentConfig =
defaultAgentConfig
{ tcpPort = undefined , -- agent does not listen to TCP
2022-04-21 20:04:22 +01:00
dbFile = " simplex_v1 " ,
2022-02-07 15:19:34 +04:00
dbPoolSize = 1 ,
yesToMigrations = False
2021-08-02 20:10:24 +01:00
} ,
2021-07-24 10:26:28 +01:00
dbPoolSize = 1 ,
2022-02-07 15:19:34 +04:00
yesToMigrations = False ,
2022-05-11 16:52:08 +01:00
defaultServers = InitialAgentServers { smp = _defaultSMPServers , ntf = _defaultNtfServers } ,
2022-02-25 16:29:36 +04:00
tbqSize = 64 ,
2022-02-09 20:58:02 +04:00
fileChunkSize = 15780 ,
2022-02-26 10:04:25 +00:00
subscriptionConcurrency = 16 ,
2022-02-25 16:29:36 +04:00
subscriptionEvents = False ,
2022-02-09 20:58:02 +04:00
testView = False
2021-07-07 22:46:38 +01:00
}
2022-05-11 16:52:08 +01:00
_defaultSMPServers :: NonEmpty SMPServer
_defaultSMPServers =
2022-03-10 15:45:40 +04:00
L . fromList
[ " smp://0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im " ,
" smp://SkIkI6EPd2D63F4xFKfHk7I1UGZVNn6k1QWZ5rcyr6w=@smp9.simplex.im " ,
" smp://6iIcWT_dF2zN_w5xzZEY7HI2Prbh3ldP07YTyDexPjE=@smp10.simplex.im "
]
2022-05-11 16:52:08 +01:00
_defaultNtfServers :: [ NtfServer ]
_defaultNtfServers = [ " smp://ZH1Dkt2_EQRbxUUyjLlcUjg1KAhBrqfvE0xfn7Ki0Zg=@ntf1.simplex.im " ]
2022-04-22 13:46:05 +01:00
2021-07-07 22:46:38 +01:00
logCfg :: LogConfig
logCfg = LogConfig { lc_file = Nothing , lc_stderr = True }
2022-04-10 17:13:06 +01:00
newChatController :: SQLiteStore -> Maybe User -> ChatConfig -> ChatOpts -> Maybe ( Notification -> IO () ) -> IO ChatController
2022-05-11 16:52:08 +01:00
newChatController chatStore user cfg @ ChatConfig { agentConfig = aCfg , tbqSize , defaultServers } ChatOpts { dbFilePrefix , smpServers , logConnections } sendToast = do
2022-01-21 11:09:33 +00:00
let f = chatStoreFile dbFilePrefix
2022-04-10 17:13:06 +01:00
config = cfg { subscriptionEvents = logConnections }
sendNotification = fromMaybe ( const $ pure () ) sendToast
2022-01-21 11:09:33 +00:00
activeTo <- newTVarIO ActiveNone
2021-12-13 12:05:57 +00:00
firstTime <- not <$> doesFileExist f
2022-01-21 11:09:33 +00:00
currentUser <- newTVarIO user
2022-05-11 16:52:08 +01:00
servers <- resolveServers defaultServers
2022-04-21 20:04:22 +01:00
smpAgent <- getSMPAgentClient aCfg { dbFile = dbFilePrefix <> " _agent.db " } servers
2022-02-06 16:18:01 +00:00
agentAsync <- newTVarIO Nothing
2021-07-12 19:00:03 +01:00
idsDrg <- newTVarIO =<< drgNew
2021-08-02 20:10:24 +01:00
inputQ <- newTBQueueIO tbqSize
2022-01-21 11:09:33 +00:00
outputQ <- newTBQueueIO tbqSize
2021-08-02 20:10:24 +01:00
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
2022-05-04 13:31:00 +01:00
currentCalls <- atomically TM . empty
2022-04-15 09:36:38 +04:00
filesFolder <- newTVarIO Nothing
2022-05-04 13:31:00 +01:00
pure ChatController { activeTo , firstTime , currentUser , smpAgent , agentAsync , chatStore , idsDrg , inputQ , outputQ , notifyQ , chatLock , sndFiles , rcvFiles , currentCalls , config , sendNotification , filesFolder }
2022-03-10 15:45:40 +04:00
where
2022-05-11 16:52:08 +01:00
resolveServers :: InitialAgentServers -> IO InitialAgentServers
resolveServers ss @ InitialAgentServers { smp = defaultSMPServers } = case nonEmpty smpServers of
Just smpServers' -> pure ss { smp = smpServers' }
_ -> case user of
Just usr -> do
userSmpServers <- getSMPServers chatStore usr
pure ss { smp = fromMaybe defaultSMPServers $ nonEmpty userSmpServers }
_ -> pure ss
2021-07-07 22:46:38 +01:00
2022-02-06 16:18:01 +00:00
runChatController :: ( MonadUnliftIO m , MonadReader ChatController m ) => User -> m ()
runChatController = race_ notificationSubscriber . agentSubscriber
startChatController :: ( MonadUnliftIO m , MonadReader ChatController m ) => User -> m ( Async () )
startChatController user = do
s <- asks agentAsync
readTVarIO s >>= maybe ( start s ) pure
where
start s = do
a <- async $ runChatController user
atomically . writeTVar s $ Just a
pure a
2022-01-24 16:07:17 +00:00
2022-04-25 16:30:21 +01:00
stopChatController :: MonadUnliftIO m => ChatController -> m ()
stopChatController ChatController { smpAgent , agentAsync = s } = do
disconnectAgentClient smpAgent
readTVarIO s >>= mapM_ uninterruptibleCancel >> atomically ( writeTVar s Nothing )
2022-01-24 16:07:17 +00:00
withLock :: MonadUnliftIO m => TMVar () -> m a -> m a
2021-08-05 20:51:48 +01:00
withLock lock =
E . bracket_
( void . atomically $ takeTMVar lock )
( atomically $ putTMVar lock () )
2022-02-04 12:41:43 +00:00
execChatCommand :: ( MonadUnliftIO m , MonadReader ChatController m ) => ByteString -> m ChatResponse
2022-04-10 12:18:53 +01:00
execChatCommand s = case parseChatCommand s of
2022-02-06 16:18:01 +00:00
Left e -> pure $ chatCmdError e
Right cmd -> either CRChatCmdError id <$> runExceptT ( processChatCommand cmd )
2022-01-24 16:07:17 +00:00
2022-04-10 12:18:53 +01:00
parseChatCommand :: ByteString -> Either String ChatCommand
parseChatCommand = parseAll chatCommandP . B . dropWhileEnd isSpace
2022-01-24 16:07:17 +00:00
toView :: ChatMonad m => ChatResponse -> m ()
toView event = do
q <- asks outputQ
2022-01-26 21:20:08 +00:00
atomically $ writeTBQueue q ( Nothing , event )
2022-01-24 16:07:17 +00:00
2022-02-06 16:18:01 +00:00
processChatCommand :: forall m . ChatMonad m => ChatCommand -> m ChatResponse
processChatCommand = \ case
ShowActiveUser -> withUser' $ pure . CRActiveUser
CreateActiveUser p -> do
u <- asks currentUser
whenM ( isJust <$> readTVarIO u ) $ throwChatError CEActiveUserExists
user <- withStore $ \ st -> createUser st p True
atomically . writeTVar u $ Just user
pure $ CRActiveUser user
2022-02-26 20:21:32 +00:00
StartChat -> withUser' $ \ user ->
asks agentAsync >>= readTVarIO >>= \ case
Just _ -> pure CRChatRunning
_ -> startChatController user $> CRChatStarted
2022-04-30 12:47:50 +01:00
ResubscribeAllConnections -> withUser ( subscribeUserConnections resubscribeConnection ) $> CRCmdOk
2022-04-15 13:16:34 +01:00
SetFilesFolder filesFolder' -> withUser $ \ _ -> do
2022-04-15 09:36:38 +04:00
createDirectoryIfMissing True filesFolder'
ff <- asks filesFolder
atomically . writeTVar ff $ Just filesFolder'
pure CRCmdOk
2022-04-23 17:32:40 +01:00
APIGetChats withPCC -> CRApiChats <$> withUser ( \ user -> withStore $ \ st -> getChatPreviews st user withPCC )
2022-04-28 08:34:21 +01:00
APIGetChat ( ChatRef cType cId ) pagination -> withUser $ \ user -> case cType of
2022-02-01 15:05:27 +04:00
CTDirect -> CRApiChat . AChat SCTDirect <$> withStore ( \ st -> getDirectChat st user cId pagination )
CTGroup -> CRApiChat . AChat SCTGroup <$> withStore ( \ st -> getGroupChat st user cId pagination )
2022-02-06 16:18:01 +00:00
CTContactRequest -> pure $ chatCmdError " not implemented "
2022-04-23 17:32:40 +01:00
CTContactConnection -> pure $ chatCmdError " not supported "
2022-02-06 16:18:01 +00:00
APIGetChatItems _pagination -> pure $ chatCmdError " not implemented "
2022-05-05 14:04:03 +01:00
APISendMessage ( ChatRef cType chatId ) ( ComposedMessage file_ quotedItemId_ mc ) -> withUser $ \ user @ User { userId } -> withChatLock $ case cType of
2022-01-30 10:49:13 +00:00
CTDirect -> do
2022-04-10 13:30:58 +04:00
ct @ Contact { localDisplayName = c } <- withStore $ \ st -> getContact st userId chatId
( fileInvitation_ , ciFile_ ) <- unzipMaybe <$> setupSndFileTransfer ct
( msgContainer , quotedItem_ ) <- prepareMsg fileInvitation_
msg <- sendDirectContactMessage ct ( XMsgNew msgContainer )
ci <- saveSndChatItem user ( CDDirectSnd ct ) msg ( CISndMsgContent mc ) ciFile_ quotedItem_
setActive $ ActiveC c
pure . CRNewChatItem $ AChatItem SCTDirect SMDSnd ( DirectChat ct ) ci
where
setupSndFileTransfer :: Contact -> m ( Maybe ( FileInvitation , CIFile 'MDSnd ) )
setupSndFileTransfer ct = case file_ of
Nothing -> pure Nothing
Just file -> do
( fileSize , chSize ) <- checkSndFile file
( agentConnId , fileConnReq ) <- withAgent ( ` createConnection ` SCMInvitation )
let fileName = takeFileName file
fileInvitation = FileInvitation { fileName , fileSize , fileConnReq = Just fileConnReq }
fileId <- withStore $ \ st -> createSndFileTransfer st userId ct file fileInvitation agentConnId chSize
let ciFile = CIFile { fileId , fileName , fileSize , filePath = Just file , fileStatus = CIFSSndStored }
pure $ Just ( fileInvitation , ciFile )
prepareMsg :: Maybe FileInvitation -> m ( MsgContainer , Maybe ( CIQuote 'CTDirect ) )
prepareMsg fileInvitation_ = case quotedItemId_ of
Nothing -> pure ( MCSimple ( ExtMsgContent mc fileInvitation_ ) , Nothing )
Just quotedItemId -> do
2022-05-05 11:52:32 +01:00
CChatItem _ ChatItem { meta = CIMeta { itemTs , itemSharedMsgId } , content = ciContent , formattedText , file } <-
2022-04-10 13:30:58 +04:00
withStore $ \ st -> getDirectChatItem st userId chatId quotedItemId
2022-04-11 09:34:59 +01:00
( origQmc , qd , sent ) <- quoteData ciContent
2022-04-10 13:30:58 +04:00
let msgRef = MsgRef { msgId = itemSharedMsgId , sentAt = itemTs , sent , memberId = Nothing }
2022-05-06 12:04:53 +04:00
qmc = quoteContent origQmc file
2022-04-10 13:30:58 +04:00
quotedItem = CIQuote { chatDir = qd , itemId = Just quotedItemId , sharedMsgId = itemSharedMsgId , sentAt = itemTs , content = qmc , formattedText }
pure ( MCQuote QuotedMsg { msgRef , content = qmc } ( ExtMsgContent mc fileInvitation_ ) , Just quotedItem )
2022-03-16 13:20:47 +00:00
where
2022-04-11 09:34:59 +01:00
quoteData :: CIContent d -> m ( MsgContent , CIQDirection 'CTDirect , Bool )
quoteData ( CISndMsgContent qmc ) = pure ( qmc , CIQDirectSnd , True )
quoteData ( CIRcvMsgContent qmc ) = pure ( qmc , CIQDirectRcv , False )
quoteData _ = throwChatError CEInvalidQuote
2022-03-13 19:34:03 +00:00
CTGroup -> do
2022-04-10 13:30:58 +04:00
Group gInfo @ GroupInfo { membership , localDisplayName = gName } ms <- withStore $ \ st -> getGroup st user chatId
2022-03-13 19:34:03 +00:00
unless ( memberActive membership ) $ throwChatError CEGroupMemberUserRemoved
2022-04-10 13:30:58 +04:00
( fileInvitation_ , ciFile_ ) <- unzipMaybe <$> setupSndFileTransfer gInfo
( msgContainer , quotedItem_ ) <- prepareMsg fileInvitation_ membership
msg <- sendGroupMessage gInfo ms ( XMsgNew msgContainer )
ci <- saveSndChatItem user ( CDGroupSnd gInfo ) msg ( CISndMsgContent mc ) ciFile_ quotedItem_
setActive $ ActiveG gName
pure . CRNewChatItem $ AChatItem SCTGroup SMDSnd ( GroupChat gInfo ) ci
where
setupSndFileTransfer :: GroupInfo -> m ( Maybe ( FileInvitation , CIFile 'MDSnd ) )
setupSndFileTransfer gInfo = case file_ of
Nothing -> pure Nothing
Just file -> do
( fileSize , chSize ) <- checkSndFile file
let fileName = takeFileName file
fileInvitation = FileInvitation { fileName , fileSize , fileConnReq = Nothing }
2022-05-11 16:18:28 +04:00
fileId <- withStore $ \ st -> createSndGroupFileTransfer st userId gInfo file fileInvitation chSize
2022-04-10 13:30:58 +04:00
let ciFile = CIFile { fileId , fileName , fileSize , filePath = Just file , fileStatus = CIFSSndStored }
pure $ Just ( fileInvitation , ciFile )
prepareMsg :: Maybe FileInvitation -> GroupMember -> m ( MsgContainer , Maybe ( CIQuote 'CTGroup ) )
prepareMsg fileInvitation_ membership = case quotedItemId_ of
Nothing -> pure ( MCSimple ( ExtMsgContent mc fileInvitation_ ) , Nothing )
Just quotedItemId -> do
2022-05-05 11:52:32 +01:00
CChatItem _ ChatItem { chatDir , meta = CIMeta { itemTs , itemSharedMsgId } , content = ciContent , formattedText , file } <-
2022-04-10 13:30:58 +04:00
withStore $ \ st -> getGroupChatItem st user chatId quotedItemId
2022-04-11 09:34:59 +01:00
( origQmc , qd , sent , GroupMember { memberId } ) <- quoteData ciContent chatDir membership
2022-04-10 13:30:58 +04:00
let msgRef = MsgRef { msgId = itemSharedMsgId , sentAt = itemTs , sent , memberId = Just memberId }
2022-05-06 12:04:53 +04:00
qmc = quoteContent origQmc file
2022-04-10 13:30:58 +04:00
quotedItem = CIQuote { chatDir = qd , itemId = Just quotedItemId , sharedMsgId = itemSharedMsgId , sentAt = itemTs , content = qmc , formattedText }
pure ( MCQuote QuotedMsg { msgRef , content = qmc } ( ExtMsgContent mc fileInvitation_ ) , Just quotedItem )
2022-03-16 13:20:47 +00:00
where
2022-04-11 09:34:59 +01:00
quoteData :: CIContent d -> CIDirection 'CTGroup d -> GroupMember -> m ( MsgContent , CIQDirection 'CTGroup , Bool , GroupMember )
quoteData ( CISndMsgContent qmc ) CIGroupSnd membership' = pure ( qmc , CIQGroupSnd , True , membership' )
quoteData ( CIRcvMsgContent qmc ) ( CIGroupRcv m ) _ = pure ( qmc , CIQGroupRcv $ Just m , False , m )
quoteData _ _ _ = throwChatError CEInvalidQuote
2022-02-06 16:18:01 +00:00
CTContactRequest -> pure $ chatCmdError " not supported "
2022-04-23 17:32:40 +01:00
CTContactConnection -> pure $ chatCmdError " not supported "
2022-04-10 13:30:58 +04:00
where
2022-05-06 12:04:53 +04:00
quoteContent :: forall d . MsgContent -> Maybe ( CIFile d ) -> MsgContent
quoteContent qmc ciFile_
| replaceContent = MCText qTextOrFile
| otherwise = case qmc of
MCImage _ image -> MCImage qTextOrFile image
MCFile _ -> MCFile qTextOrFile
_ -> qmc
where
-- if the message we're quoting with is one of the "large" MsgContents
-- we replace the quote's content with MCText
replaceContent = case mc of
MCText _ -> False
MCFile _ -> False
MCLink { } -> True
MCImage { } -> True
MCUnknown { } -> True
qText = msgContentText qmc
qFileName = maybe qText ( T . pack . ( fileName :: CIFile d -> String ) ) ciFile_
qTextOrFile = if T . null qText then qFileName else qText
2022-04-10 13:30:58 +04:00
unzipMaybe :: Maybe ( a , b ) -> ( Maybe a , Maybe b )
unzipMaybe t = ( fst <$> t , snd <$> t )
2022-04-28 08:34:21 +01:00
APIUpdateChatItem ( ChatRef cType chatId ) itemId mc -> withUser $ \ user @ User { userId } -> withChatLock $ case cType of
2022-03-23 11:37:51 +00:00
CTDirect -> do
( ct @ Contact { contactId , localDisplayName = c } , ci ) <- withStore $ \ st -> ( , ) <$> getContact st userId chatId <*> getDirectChatItem st userId chatId itemId
case ci of
CChatItem SMDSnd ChatItem { meta = CIMeta { itemSharedMsgId } , content = ciContent } -> do
case ( ciContent , itemSharedMsgId ) of
( CISndMsgContent _ , Just itemSharedMId ) -> do
SndMessage { msgId } <- sendDirectContactMessage ct ( XMsgUpdate itemSharedMId mc )
2022-05-04 13:31:00 +01:00
updCi <- withStore $ \ st -> updateDirectChatItem st userId contactId itemId ( CISndMsgContent mc ) $ Just msgId
2022-03-23 11:37:51 +00:00
setActive $ ActiveC c
pure . CRChatItemUpdated $ AChatItem SCTDirect SMDSnd ( DirectChat ct ) updCi
2022-03-28 20:35:57 +04:00
_ -> throwChatError CEInvalidChatItemUpdate
CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate
2022-03-23 11:37:51 +00:00
CTGroup -> do
Group gInfo @ GroupInfo { groupId , localDisplayName = gName , membership } ms <- withStore $ \ st -> getGroup st user chatId
unless ( memberActive membership ) $ throwChatError CEGroupMemberUserRemoved
ci <- withStore $ \ st -> getGroupChatItem st user chatId itemId
case ci of
CChatItem SMDSnd ChatItem { meta = CIMeta { itemSharedMsgId } , content = ciContent } -> do
case ( ciContent , itemSharedMsgId ) of
( CISndMsgContent _ , Just itemSharedMId ) -> do
SndMessage { msgId } <- sendGroupMessage gInfo ms ( XMsgUpdate itemSharedMId mc )
updCi <- withStore $ \ st -> updateGroupChatItem st user groupId itemId ( CISndMsgContent mc ) msgId
setActive $ ActiveG gName
pure . CRChatItemUpdated $ AChatItem SCTGroup SMDSnd ( GroupChat gInfo ) updCi
2022-03-28 20:35:57 +04:00
_ -> throwChatError CEInvalidChatItemUpdate
CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate
2022-03-23 11:37:51 +00:00
CTContactRequest -> pure $ chatCmdError " not supported "
2022-04-23 17:32:40 +01:00
CTContactConnection -> pure $ chatCmdError " not supported "
2022-04-28 08:34:21 +01:00
APIDeleteChatItem ( ChatRef cType chatId ) itemId mode -> withUser $ \ user @ User { userId } -> withChatLock $ case cType of
2022-03-28 20:35:57 +04:00
CTDirect -> do
2022-04-15 09:36:38 +04:00
( ct @ Contact { localDisplayName = c } , CChatItem msgDir deletedItem @ ChatItem { meta = CIMeta { itemSharedMsgId } , file } ) <- withStore $ \ st -> ( , ) <$> getContact st userId chatId <*> getDirectChatItem st userId chatId itemId
2022-03-28 20:35:57 +04:00
case ( mode , msgDir , itemSharedMsgId ) of
( CIDMInternal , _ , _ ) -> do
2022-05-11 16:18:28 +04:00
deleteCIFile user file
2022-05-17 11:22:09 +04:00
toCi <- withStore $ \ st -> deleteDirectChatItemLocal st userId ct itemId CIDMInternal
2022-03-28 20:35:57 +04:00
pure $ CRChatItemDeleted ( AChatItem SCTDirect msgDir ( DirectChat ct ) deletedItem ) toCi
( CIDMBroadcast , SMDSnd , Just itemSharedMId ) -> do
2022-05-17 11:22:09 +04:00
void $ sendDirectContactMessage ct ( XMsgDel itemSharedMId )
2022-05-11 16:18:28 +04:00
deleteCIFile user file
2022-05-17 11:22:09 +04:00
toCi <- withStore $ \ st -> deleteDirectChatItemLocal st userId ct itemId CIDMBroadcast
2022-03-28 20:35:57 +04:00
setActive $ ActiveC c
pure $ CRChatItemDeleted ( AChatItem SCTDirect msgDir ( DirectChat ct ) deletedItem ) toCi
( CIDMBroadcast , _ , _ ) -> throwChatError CEInvalidChatItemDelete
2022-05-17 11:22:09 +04:00
-- TODO for group integrity and pending messages, group items and messages are set to "deleted"; maybe a different workaround is needed
2022-03-28 20:35:57 +04:00
CTGroup -> do
Group gInfo @ GroupInfo { localDisplayName = gName , membership } ms <- withStore $ \ st -> getGroup st user chatId
unless ( memberActive membership ) $ throwChatError CEGroupMemberUserRemoved
2022-04-15 13:16:34 +01:00
CChatItem msgDir deletedItem @ ChatItem { meta = CIMeta { itemSharedMsgId } , file } <- withStore $ \ st -> getGroupChatItem st user chatId itemId
2022-03-28 20:35:57 +04:00
case ( mode , msgDir , itemSharedMsgId ) of
( CIDMInternal , _ , _ ) -> do
2022-05-11 16:18:28 +04:00
deleteCIFile user file
2022-03-28 20:35:57 +04:00
toCi <- withStore $ \ st -> deleteGroupChatItemInternal st user gInfo itemId
pure $ CRChatItemDeleted ( AChatItem SCTGroup msgDir ( GroupChat gInfo ) deletedItem ) toCi
( CIDMBroadcast , SMDSnd , Just itemSharedMId ) -> do
SndMessage { msgId } <- sendGroupMessage gInfo ms ( XMsgDel itemSharedMId )
2022-05-11 16:18:28 +04:00
deleteCIFile user file
2022-03-28 20:35:57 +04:00
toCi <- withStore $ \ st -> deleteGroupChatItemSndBroadcast st user gInfo itemId msgId
setActive $ ActiveG gName
pure $ CRChatItemDeleted ( AChatItem SCTGroup msgDir ( GroupChat gInfo ) deletedItem ) toCi
( CIDMBroadcast , _ , _ ) -> throwChatError CEInvalidChatItemDelete
2022-03-23 11:37:51 +00:00
CTContactRequest -> pure $ chatCmdError " not supported "
2022-04-23 17:32:40 +01:00
CTContactConnection -> pure $ chatCmdError " not supported "
2022-04-15 09:36:38 +04:00
where
2022-05-11 16:18:28 +04:00
deleteCIFile :: MsgDirectionI d => User -> Maybe ( CIFile d ) -> m ()
deleteCIFile user file =
2022-04-15 13:16:34 +01:00
forM_ file $ \ CIFile { fileId , filePath , fileStatus } -> do
2022-05-17 11:22:09 +04:00
let fileInfo = CIFileInfo { fileId , fileStatus = AFS msgDirection fileStatus , filePath }
cancelFile user fileInfo
withFilesFolder $ \ filesFolder -> deleteFile filesFolder fileInfo
2022-04-28 08:34:21 +01:00
APIChatRead ( ChatRef cType chatId ) fromToIds -> withChatLock $ case cType of
2022-02-08 17:27:43 +04:00
CTDirect -> withStore ( \ st -> updateDirectChatItemsRead st chatId fromToIds ) $> CRCmdOk
CTGroup -> withStore ( \ st -> updateGroupChatItemsRead st chatId fromToIds ) $> CRCmdOk
CTContactRequest -> pure $ chatCmdError " not supported "
2022-04-23 17:32:40 +01:00
CTContactConnection -> pure $ chatCmdError " not supported "
2022-05-05 10:37:53 +01:00
APIDeleteChat ( ChatRef cType chatId ) -> withUser $ \ user @ User { userId } -> case cType of
2022-01-31 21:53:53 +04:00
CTDirect -> do
ct @ Contact { localDisplayName } <- withStore $ \ st -> getContact st userId chatId
withStore ( \ st -> getContactGroupNames st userId ct ) >>= \ case
[] -> do
2022-05-17 11:22:09 +04:00
filesInfo <- withStore $ \ st -> getContactFileInfo st userId ct
2022-01-31 21:53:53 +04:00
conns <- withStore $ \ st -> getContactConnections st userId ct
2022-02-06 08:21:40 +00:00
withChatLock . procCmd $ do
2022-05-17 11:22:09 +04:00
forM_ filesInfo $ \ fileInfo -> do
cancelFile user fileInfo
withFilesFolder $ \ filesFolder -> deleteFile filesFolder fileInfo
2022-05-09 10:55:56 +04:00
withAgent $ \ a -> forM_ conns $ \ conn ->
2022-01-31 21:53:53 +04:00
deleteConnection a ( aConnId conn ) ` catchError ` \ ( _ :: AgentErrorType ) -> pure ()
withStore $ \ st -> deleteContact st userId ct
unsetActive $ ActiveC localDisplayName
pure $ CRContactDeleted ct
gs -> throwChatError $ CEContactGroups ct gs
2022-04-25 10:39:28 +01:00
CTContactConnection -> withChatLock . procCmd $ do
conn <- withStore $ \ st -> getPendingContactConnection st userId chatId
withAgent $ \ a -> deleteConnection a $ aConnId' conn
withStore $ \ st -> deletePendingContactConnection st userId chatId
pure $ CRContactConnectionDeleted conn
2022-02-06 16:18:01 +00:00
CTGroup -> pure $ chatCmdError " not implemented "
CTContactRequest -> pure $ chatCmdError " not supported "
2022-05-17 11:22:09 +04:00
APIClearChat ( ChatRef cType chatId ) -> withUser $ \ user @ User { userId } -> case cType of
CTDirect -> do
ct <- withStore $ \ st -> getContact st userId chatId
ciIdsAndFileInfo <- withStore $ \ st -> getContactChatItemIdsAndFileInfo st userId chatId
forM_ ciIdsAndFileInfo $ \ ( itemId , fileInfo_ ) -> do
forM_ fileInfo_ $ \ fileInfo -> do
cancelFile user fileInfo
withFilesFolder $ \ filesFolder -> deleteFile filesFolder fileInfo
void $ withStore $ \ st -> deleteDirectChatItemLocal st userId ct itemId CIDMInternal
pure $ CRChatCleared ( AChatInfo SCTDirect ( DirectChat ct ) )
CTGroup -> do
gInfo <- withStore $ \ st -> getGroupInfo st user chatId
ciIdsAndFileInfo <- withStore $ \ st -> getGroupChatItemIdsAndFileInfo st userId chatId
2022-05-17 22:48:54 +04:00
forM_ ciIdsAndFileInfo $ \ ( itemId , itemDeleted , fileInfo_ ) ->
unless itemDeleted $ do
forM_ fileInfo_ $ \ fileInfo -> do
cancelFile user fileInfo
withFilesFolder $ \ filesFolder -> deleteFile filesFolder fileInfo
void $ withStore $ \ st -> deleteGroupChatItemInternal st user gInfo itemId
2022-05-17 11:22:09 +04:00
pure $ CRChatCleared ( AChatInfo SCTGroup ( GroupChat gInfo ) )
CTContactConnection -> pure $ chatCmdError " not supported "
CTContactRequest -> pure $ chatCmdError " not supported "
2022-02-14 14:59:11 +04:00
APIAcceptContact connReqId -> withUser $ \ user @ User { userId } -> withChatLock $ do
cReq <- withStore $ \ st -> getContactRequest st userId connReqId
procCmd $ CRAcceptingContactRequest <$> acceptContactRequest user cReq
2022-02-06 16:18:01 +00:00
APIRejectContact connReqId -> withUser $ \ User { userId } -> withChatLock $ do
2022-02-01 05:31:34 +00:00
cReq @ UserContactRequest { agentContactConnId = AgentConnId connId , agentInvitationId = AgentInvId invId } <-
withStore $ \ st ->
getContactRequest st userId connReqId
` E . finally ` deleteContactRequest st userId connReqId
withAgent $ \ a -> rejectContact a connId invId
pure $ CRContactRequestRejected cReq
2022-05-18 07:01:32 +01:00
APISendCallInvitation contactId callType -> withUser $ \ user @ User { userId } -> do
2022-05-03 10:22:35 +01:00
-- party initiating call
ct <- withStore $ \ st -> getContact st userId contactId
2022-05-04 13:31:00 +01:00
calls <- asks currentCalls
withChatLock $ do
callId <- CallId <$> ( asks idsDrg >>= liftIO . ( ` randomBytes ` 16 ) )
2022-05-18 07:01:32 +01:00
dhKeyPair <- if encryptedCall callType then Just <$> liftIO C . generateKeyPair' else pure Nothing
2022-05-04 13:31:00 +01:00
let invitation = CallInvitation { callType , callDhPubKey = fst <$> dhKeyPair }
callState = CallInvitationSent { localCallType = callType , localDhPrivKey = snd <$> dhKeyPair }
msg @ SndMessage { msgId } <- sendDirectContactMessage ct ( XCallInv callId invitation )
ci <- saveSndChatItem user ( CDDirectSnd ct ) msg ( CISndCall CISCallPending 0 ) Nothing Nothing
let call' = Call { contactId , callId , chatItemId = chatItemId' ci , callState }
call_ <- atomically $ TM . lookupInsert contactId call' calls
forM_ call_ $ \ call -> updateCallItemStatus userId ct call WCSDisconnected $ Just msgId
toView . CRNewChatItem $ AChatItem SCTDirect SMDSnd ( DirectChat ct ) ci
pure CRCmdOk
2022-05-17 08:37:00 +01:00
SendCallInvitation cName callType -> withUser $ \ User { userId } -> do
contactId <- withStore $ \ st -> getContactIdByName st userId cName
processChatCommand $ APISendCallInvitation contactId callType
2022-05-03 10:22:35 +01:00
APIRejectCall contactId ->
-- party accepting call
withCurrentCall contactId $ \ userId ct Call { chatItemId , callState } -> case callState of
2022-05-04 13:31:00 +01:00
CallInvitationReceived { } ->
let aciContent = ACIContent SMDRcv $ CIRcvCall CISCallRejected 0
in updateDirectChatItemView userId ct chatItemId aciContent Nothing $> Nothing
2022-05-03 10:22:35 +01:00
_ -> throwChatError . CECallState $ callStateTag callState
APISendCallOffer contactId WebRTCCallOffer { callType , rtcSession } ->
-- party accepting call
withCurrentCall contactId $ \ userId ct call @ Call { callId , chatItemId , callState } -> case callState of
CallInvitationReceived { peerCallType , localDhPubKey , sharedKey } -> do
2022-05-18 07:01:32 +01:00
let callDhPubKey = if encryptedCall callType then localDhPubKey else Nothing
offer = CallOffer { callType , rtcSession , callDhPubKey }
2022-05-03 10:22:35 +01:00
callState' = CallOfferSent { localCallType = callType , peerCallType , localCallSession = rtcSession , sharedKey }
2022-05-04 13:31:00 +01:00
aciContent = ACIContent SMDRcv $ CIRcvCall CISCallAccepted 0
2022-05-03 10:22:35 +01:00
SndMessage { msgId } <- sendDirectContactMessage ct ( XCallOffer callId offer )
2022-05-04 13:31:00 +01:00
updateDirectChatItemView userId ct chatItemId aciContent $ Just msgId
2022-05-03 10:22:35 +01:00
pure $ Just call { callState = callState' }
_ -> throwChatError . CECallState $ callStateTag callState
APISendCallAnswer contactId rtcSession ->
-- party initiating call
withCurrentCall contactId $ \ userId ct call @ Call { callId , chatItemId , callState } -> case callState of
CallOfferReceived { localCallType , peerCallType , peerCallSession , sharedKey } -> do
let callState' = CallNegotiated { localCallType , peerCallType , localCallSession = rtcSession , peerCallSession , sharedKey }
2022-05-04 13:31:00 +01:00
aciContent = ACIContent SMDSnd $ CISndCall CISCallNegotiated 0
SndMessage { msgId } <- sendDirectContactMessage ct ( XCallAnswer callId CallAnswer { rtcSession } )
updateDirectChatItemView userId ct chatItemId aciContent $ Just msgId
2022-05-03 10:22:35 +01:00
pure $ Just call { callState = callState' }
_ -> throwChatError . CECallState $ callStateTag callState
APISendCallExtraInfo contactId rtcExtraInfo ->
-- any call party
withCurrentCall contactId $ \ _ ct call @ Call { callId , callState } -> case callState of
2022-05-07 06:40:46 +01:00
CallOfferSent { localCallType , peerCallType , localCallSession , sharedKey } -> do
-- TODO update the list of ice servers in localCallSession
_ <- sendDirectContactMessage ct ( XCallExtra callId CallExtraInfo { rtcExtraInfo } )
let callState' = CallOfferSent { localCallType , peerCallType , localCallSession , sharedKey }
pure $ Just call { callState = callState' }
2022-05-03 10:22:35 +01:00
CallNegotiated { localCallType , peerCallType , localCallSession , peerCallSession , sharedKey } -> do
2022-05-07 06:40:46 +01:00
-- TODO update the list of ice servers in localCallSession
2022-05-03 10:22:35 +01:00
_ <- sendDirectContactMessage ct ( XCallExtra callId CallExtraInfo { rtcExtraInfo } )
let callState' = CallNegotiated { localCallType , peerCallType , localCallSession , peerCallSession , sharedKey }
pure $ Just call { callState = callState' }
_ -> throwChatError . CECallState $ callStateTag callState
APIEndCall contactId ->
-- any call party
2022-05-04 13:31:00 +01:00
withCurrentCall contactId $ \ userId ct call @ Call { callId } -> do
2022-05-03 10:22:35 +01:00
SndMessage { msgId } <- sendDirectContactMessage ct ( XCallEnd callId )
2022-05-04 13:31:00 +01:00
updateCallItemStatus userId ct call WCSDisconnected $ Just msgId
2022-05-03 10:22:35 +01:00
pure Nothing
2022-05-04 13:31:00 +01:00
APICallStatus contactId receivedStatus ->
withCurrentCall contactId $ \ userId ct call ->
updateCallItemStatus userId ct call receivedStatus Nothing $> Just call
2022-03-23 20:52:00 +00:00
APIUpdateProfile profile -> withUser ( ` updateProfile ` profile )
2022-04-04 19:51:49 +01:00
APIParseMarkdown text -> pure . CRApiParsedMarkdown $ parseMaybeMarkdownList text
2022-04-22 20:32:19 +01:00
APIRegisterToken token -> CRNtfTokenStatus <$> withUser ( \ _ -> withAgent ( ` registerNtfToken ` token ) )
2022-04-21 20:04:22 +01:00
APIVerifyToken token code nonce -> withUser $ \ _ -> withAgent ( \ a -> verifyNtfToken a token code nonce ) $> CRCmdOk
APIIntervalNofication token interval -> withUser $ \ _ -> withAgent ( \ a -> enableNtfCron a token interval ) $> CRCmdOk
APIDeleteToken token -> withUser $ \ _ -> withAgent ( ` deleteNtfToken ` token ) $> CRCmdOk
2022-03-10 15:45:40 +04:00
GetUserSMPServers -> CRUserSMPServers <$> withUser ( \ user -> withStore ( ` getSMPServers ` user ) )
SetUserSMPServers smpServers -> withUser $ \ user -> withChatLock $ do
withStore $ \ st -> overwriteSMPServers st user smpServers
2022-05-11 16:52:08 +01:00
ChatConfig { defaultServers = InitialAgentServers { smp = defaultSMPServers } } <- asks config
2022-03-10 15:45:40 +04:00
withAgent $ \ a -> setSMPServers a ( fromMaybe defaultSMPServers ( nonEmpty smpServers ) )
pure CRCmdOk
2022-01-24 16:07:17 +00:00
ChatHelp section -> pure $ CRChatHelp section
2022-02-06 16:18:01 +00:00
Welcome -> withUser $ pure . CRWelcome
AddContact -> withUser $ \ User { userId } -> withChatLock . procCmd $ do
2021-12-08 13:09:51 +00:00
( connId , cReq ) <- withAgent ( ` createConnection ` SCMInvitation )
2022-04-23 17:32:40 +01:00
conn <- withStore $ \ st -> createDirectConnection st userId connId ConnNew
toView $ CRNewContactConnection conn
2022-01-24 16:07:17 +00:00
pure $ CRInvitation cReq
2022-02-06 16:18:01 +00:00
Connect ( Just ( ACR SCMInvitation cReq ) ) -> withUser $ \ User { userId , profile } -> withChatLock . procCmd $ do
2022-02-13 13:19:24 +04:00
connId <- withAgent $ \ a -> joinConnection a cReq . directMessage $ XInfo profile
2022-04-23 17:32:40 +01:00
conn <- withStore $ \ st -> createDirectConnection st userId connId ConnJoined
toView $ CRNewContactConnection conn
2022-01-24 16:07:17 +00:00
pure CRSentConfirmation
2022-02-13 13:19:24 +04:00
Connect ( Just ( ACR SCMContact cReq ) ) -> withUser $ \ User { userId , profile } ->
connectViaContact userId cReq profile
2022-01-26 21:20:08 +00:00
Connect Nothing -> throwChatError CEInvalidConnReq
2022-03-29 08:53:30 +01:00
ConnectSimplex -> withUser $ \ User { userId , profile } ->
2022-02-13 13:19:24 +04:00
connectViaContact userId adminContactReq profile
2022-02-06 16:18:01 +00:00
DeleteContact cName -> withUser $ \ User { userId } -> do
2022-01-31 15:14:56 +04:00
contactId <- withStore $ \ st -> getContactIdByName st userId cName
2022-04-28 08:34:21 +01:00
processChatCommand $ APIDeleteChat ( ChatRef CTDirect contactId )
2022-05-17 11:22:09 +04:00
ClearContact cName -> withUser $ \ User { userId } -> do
contactId <- withStore $ \ st -> getContactIdByName st userId cName
processChatCommand $ APIClearChat ( ChatRef CTDirect contactId )
2022-02-06 16:18:01 +00:00
ListContacts -> withUser $ \ user -> CRContactsList <$> withStore ( ` getUserContacts ` user )
CreateMyAddress -> withUser $ \ User { userId } -> withChatLock . procCmd $ do
2021-12-08 13:09:51 +00:00
( connId , cReq ) <- withAgent ( ` createConnection ` SCMContact )
withStore $ \ st -> createUserContactLink st userId connId cReq
2022-01-24 16:07:17 +00:00
pure $ CRUserContactLinkCreated cReq
2022-02-06 16:18:01 +00:00
DeleteMyAddress -> withUser $ \ User { userId } -> withChatLock $ do
2021-12-08 13:09:51 +00:00
conns <- withStore $ \ st -> getUserContactLinkConnections st userId
2022-01-24 16:07:17 +00:00
procCmd $ do
2022-01-26 21:20:08 +00:00
withAgent $ \ a -> forM_ conns $ \ conn ->
deleteConnection a ( aConnId conn ) ` catchError ` \ ( _ :: AgentErrorType ) -> pure ()
2022-01-24 16:07:17 +00:00
withStore $ \ st -> deleteUserContactLink st userId
pure CRUserContactLinkDeleted
2022-02-14 14:59:11 +04:00
ShowMyAddress -> withUser $ \ User { userId } ->
uncurry CRUserContactLink <$> withStore ( ` getUserContactLink ` userId )
AddressAutoAccept onOff -> withUser $ \ User { userId } -> do
uncurry CRUserContactLinkUpdated <$> withStore ( \ st -> updateUserContactLinkAutoAccept st userId onOff )
2022-02-06 16:18:01 +00:00
AcceptContact cName -> withUser $ \ User { userId } -> do
2022-02-01 05:31:34 +00:00
connReqId <- withStore $ \ st -> getContactRequestIdByName st userId cName
2022-02-06 16:18:01 +00:00
processChatCommand $ APIAcceptContact connReqId
RejectContact cName -> withUser $ \ User { userId } -> do
2022-02-01 05:31:34 +00:00
connReqId <- withStore $ \ st -> getContactRequestIdByName st userId cName
2022-02-06 16:18:01 +00:00
processChatCommand $ APIRejectContact connReqId
2022-04-28 08:34:21 +01:00
SendMessage chatName msg -> withUser $ \ user -> do
chatRef <- getChatRef user chatName
2022-01-26 16:18:27 +04:00
let mc = MCText $ safeDecodeUtf8 msg
2022-05-05 14:04:03 +01:00
processChatCommand . APISendMessage chatRef $ ComposedMessage Nothing Nothing mc
2022-03-29 08:53:30 +01:00
SendMessageBroadcast msg -> withUser $ \ user -> do
contacts <- withStore ( ` getUserContacts ` user )
withChatLock . procCmd $ do
let mc = MCText $ safeDecodeUtf8 msg
cts = filter isReady contacts
forM_ cts $ \ ct ->
2022-04-10 13:30:58 +04:00
void
( do
sndMsg <- sendDirectContactMessage ct ( XMsgNew $ MCSimple ( ExtMsgContent mc Nothing ) )
saveSndChatItem user ( CDDirectSnd ct ) sndMsg ( CISndMsgContent mc ) Nothing Nothing
)
2022-03-29 08:53:30 +01:00
` catchError ` ( toView . CRChatError )
CRBroadcastSent mc ( length cts ) <$> liftIO getZonedTime
2022-03-13 19:34:03 +00:00
SendMessageQuote cName ( AMsgDirection msgDir ) quotedMsg msg -> withUser $ \ User { userId } -> do
contactId <- withStore $ \ st -> getContactIdByName st userId cName
quotedItemId <- withStore $ \ st -> getDirectChatItemIdByText st userId contactId msgDir ( safeDecodeUtf8 quotedMsg )
let mc = MCText $ safeDecodeUtf8 msg
2022-05-05 14:04:03 +01:00
processChatCommand . APISendMessage ( ChatRef CTDirect contactId ) $ ComposedMessage Nothing ( Just quotedItemId ) mc
2022-04-28 08:34:21 +01:00
DeleteMessage chatName deletedMsg -> withUser $ \ user -> do
chatRef <- getChatRef user chatName
deletedItemId <- getSentChatItemIdByText user chatRef deletedMsg
processChatCommand $ APIDeleteChatItem chatRef deletedItemId CIDMBroadcast
EditMessage chatName editedMsg msg -> withUser $ \ user -> do
chatRef <- getChatRef user chatName
editedItemId <- getSentChatItemIdByText user chatRef editedMsg
2022-04-03 09:44:23 +01:00
let mc = MCText $ safeDecodeUtf8 msg
2022-04-28 08:34:21 +01:00
processChatCommand $ APIUpdateChatItem chatRef editedItemId mc
2022-02-06 16:18:01 +00:00
NewGroup gProfile -> withUser $ \ user -> do
2021-07-12 19:00:03 +01:00
gVar <- asks idsDrg
2022-01-24 16:07:17 +00:00
CRGroupCreated <$> withStore ( \ st -> createNewGroup st gVar user gProfile )
2022-02-06 16:18:01 +00:00
AddMember gName cName memRole -> withUser $ \ user @ User { userId } -> withChatLock $ do
2022-01-26 16:18:27 +04:00
-- TODO for large groups: no need to load all members to determine if contact is a member
2022-01-30 10:49:13 +00:00
( group , contact ) <- withStore $ \ st -> ( , ) <$> getGroupByName st user gName <*> getContactByName st userId cName
2022-01-26 16:18:27 +04:00
let Group gInfo @ GroupInfo { groupId , groupProfile , membership } members = group
2022-01-06 20:29:57 +00:00
GroupMember { memberRole = userRole , memberId = userMemberId } = membership
2022-01-26 21:20:08 +00:00
when ( userRole < GRAdmin || userRole < memRole ) $ throwChatError CEGroupUserRole
when ( memberStatus membership == GSMemInvited ) $ throwChatError ( CEGroupNotJoined gInfo )
unless ( memberActive membership ) $ throwChatError CEGroupMemberNotActive
2022-01-06 20:29:57 +00:00
let sendInvitation memberId cReq = do
2022-02-14 18:49:42 +04:00
void . sendDirectContactMessage contact $
2022-01-11 08:50:44 +00:00
XGrpInv $ GroupInvitation ( MemberIdRole userMemberId userRole ) ( MemberIdRole memberId memRole ) cReq groupProfile
2022-01-06 20:29:57 +00:00
setActive $ ActiveG gName
2022-01-26 16:18:27 +04:00
pure $ CRSentGroupInvitation gInfo contact
2022-01-06 23:39:58 +04:00
case contactMember contact members of
Nothing -> do
gVar <- asks idsDrg
( agentConnId , cReq ) <- withAgent ( ` createConnection ` SCMInvitation )
2022-01-06 20:29:57 +00:00
GroupMember { memberId } <- withStore $ \ st -> createContactMember st gVar user groupId contact memRole agentConnId cReq
sendInvitation memberId cReq
2022-01-06 23:39:58 +04:00
Just GroupMember { groupMemberId , memberId , memberStatus }
| memberStatus == GSMemInvited ->
2022-01-06 20:29:57 +00:00
withStore ( \ st -> getMemberInvitation st user groupMemberId ) >>= \ case
Just cReq -> sendInvitation memberId cReq
2022-01-26 21:20:08 +00:00
Nothing -> throwChatError $ CEGroupCantResendInvitation gInfo cName
| otherwise -> throwChatError $ CEGroupDuplicateMember cName
2022-02-06 16:18:01 +00:00
JoinGroup gName -> withUser $ \ user @ User { userId } -> do
2022-01-26 16:18:27 +04:00
ReceivedGroupInvitation { fromMember , connRequest , groupInfo = g } <- withStore $ \ st -> getGroupInvitation st user gName
2022-02-06 08:21:40 +00:00
withChatLock . procCmd $ do
2022-01-26 16:18:27 +04:00
agentConnId <- withAgent $ \ a -> joinConnection a connRequest . directMessage . XGrpAcpt $ memberId ( membership g :: GroupMember )
2022-01-24 16:07:17 +00:00
withStore $ \ st -> do
createMemberConnection st userId fromMember agentConnId
updateGroupMemberStatus st userId fromMember GSMemAccepted
2022-01-26 16:18:27 +04:00
updateGroupMemberStatus st userId ( membership g ) GSMemAccepted
pure $ CRUserAcceptedGroupSent g
2022-01-26 21:20:08 +00:00
MemberRole _gName _cName _mRole -> throwChatError $ CECommandError " unsupported "
2022-02-06 16:18:01 +00:00
RemoveMember gName cName -> withUser $ \ user @ User { userId } -> do
2022-01-30 10:49:13 +00:00
Group gInfo @ GroupInfo { membership } members <- withStore $ \ st -> getGroupByName st user gName
2021-08-02 20:10:24 +01:00
case find ( ( == cName ) . ( localDisplayName :: GroupMember -> ContactName ) ) members of
2022-01-26 21:20:08 +00:00
Nothing -> throwChatError $ CEGroupMemberNotFound cName
2022-01-11 08:50:44 +00:00
Just m @ GroupMember { memberId = mId , memberRole = mRole , memberStatus = mStatus } -> do
let userRole = memberRole ( membership :: GroupMember )
2022-01-26 21:20:08 +00:00
when ( userRole < GRAdmin || userRole < mRole ) $ throwChatError CEGroupUserRole
2022-02-06 08:21:40 +00:00
withChatLock . procCmd $ do
2022-02-25 21:59:35 +04:00
when ( mStatus /= GSMemInvited ) . void . sendGroupMessage gInfo members $ XGrpMemDel mId
2022-01-24 16:07:17 +00:00
deleteMemberConnection m
withStore $ \ st -> updateGroupMemberStatus st userId m GSMemRemoved
2022-01-26 16:18:27 +04:00
pure $ CRUserDeletedMember gInfo m
2022-02-06 16:18:01 +00:00
LeaveGroup gName -> withUser $ \ user @ User { userId } -> do
2022-01-30 10:49:13 +00:00
Group gInfo @ GroupInfo { membership } members <- withStore $ \ st -> getGroupByName st user gName
2022-02-06 08:21:40 +00:00
withChatLock . procCmd $ do
2022-02-25 21:59:35 +04:00
void $ sendGroupMessage gInfo members XGrpLeave
2022-01-24 16:07:17 +00:00
mapM_ deleteMemberConnection members
withStore $ \ st -> updateGroupMemberStatus st userId membership GSMemLeft
2022-01-26 16:18:27 +04:00
pure $ CRLeftMemberUser gInfo
2022-02-06 16:18:01 +00:00
DeleteGroup gName -> withUser $ \ user -> do
2022-01-30 10:49:13 +00:00
g @ ( Group gInfo @ GroupInfo { membership } members ) <- withStore $ \ st -> getGroupByName st user gName
2021-08-02 20:10:24 +01:00
let s = memberStatus membership
canDelete =
2022-01-11 08:50:44 +00:00
memberRole ( membership :: GroupMember ) == GROwner
2022-01-05 20:46:35 +04:00
|| ( s == GSMemRemoved || s == GSMemLeft || s == GSMemGroupDeleted || s == GSMemInvited )
2022-01-26 21:20:08 +00:00
unless canDelete $ throwChatError CEGroupUserRole
2022-02-06 08:21:40 +00:00
withChatLock . procCmd $ do
2022-02-25 21:59:35 +04:00
when ( memberActive membership ) . void $ sendGroupMessage gInfo members XGrpDel
2022-01-24 16:07:17 +00:00
mapM_ deleteMemberConnection members
withStore $ \ st -> deleteGroup st user g
2022-01-26 16:18:27 +04:00
pure $ CRGroupDeletedUser gInfo
2022-05-17 11:22:09 +04:00
ClearGroup gName -> withUser $ \ user -> do
groupId <- withStore $ \ st -> getGroupIdByName st user gName
processChatCommand $ APIClearChat ( ChatRef CTGroup groupId )
2022-02-07 15:19:34 +04:00
ListMembers gName -> CRGroupMembers <$> withUser ( \ user -> withStore ( \ st -> getGroupByName st user gName ) )
2022-02-06 16:18:01 +00:00
ListGroups -> CRGroupsList <$> withUser ( \ user -> withStore ( ` getUserGroupDetails ` user ) )
2022-03-13 19:34:03 +00:00
SendGroupMessageQuote gName cName quotedMsg msg -> withUser $ \ user -> do
groupId <- withStore $ \ st -> getGroupIdByName st user gName
quotedItemId <- withStore $ \ st -> getGroupChatItemIdByText st user groupId cName ( safeDecodeUtf8 quotedMsg )
let mc = MCText $ safeDecodeUtf8 msg
2022-05-05 14:04:03 +01:00
processChatCommand . APISendMessage ( ChatRef CTGroup groupId ) $ ComposedMessage Nothing ( Just quotedItemId ) mc
2022-04-28 08:34:21 +01:00
LastMessages ( Just chatName ) count -> withUser $ \ user -> do
chatRef <- getChatRef user chatName
CRLastMessages . aChatItems . chat <$> ( processChatCommand . APIGetChat chatRef $ CPLast count )
2022-04-30 21:23:14 +01:00
LastMessages Nothing count -> withUser $ \ user -> withStore $ \ st ->
CRLastMessages <$> getAllChatItems st user ( CPLast count )
2022-04-30 19:18:46 +04:00
SendFile chatName f -> withUser $ \ user -> do
chatRef <- getChatRef user chatName
2022-05-05 14:04:03 +01:00
processChatCommand . APISendMessage chatRef $ ComposedMessage ( Just f ) Nothing ( MCFile " " )
2022-05-11 16:18:28 +04:00
ReceiveFile fileId filePath_ -> withUser $ \ user ->
2022-04-10 13:30:58 +04:00
withChatLock . procCmd $ do
2022-05-11 16:18:28 +04:00
ft <- withStore $ \ st -> getRcvFileTransfer st user fileId
2022-04-29 15:56:56 +04:00
( CRRcvFileAccepted <$> acceptFileReceive user ft filePath_ ) ` catchError ` processError ft
2022-04-10 13:30:58 +04:00
where
processError ft = \ case
2022-04-29 15:56:56 +04:00
-- TODO AChatItem in Cancelled events
2022-04-10 13:30:58 +04:00
ChatErrorAgent ( SMP SMP . AUTH ) -> pure $ CRRcvFileAcceptedSndCancelled ft
ChatErrorAgent ( CONN DUPLICATE ) -> pure $ CRRcvFileAcceptedSndCancelled ft
e -> throwError e
2022-05-11 16:18:28 +04:00
CancelFile fileId -> withUser $ \ user @ User { userId } ->
withChatLock . procCmd $
withStore ( \ st -> getFileTransfer st user fileId ) >>= \ case
FTSnd ftm @ FileTransferMeta { cancelled } fts -> do
unless cancelled $ do
cancelSndFile user ftm fts
sharedMsgId <- withStore $ \ st -> getSharedMsgIdByFileId st userId fileId
void $
withStore ( \ st -> getChatRefByFileId st user fileId ) >>= \ case
ChatRef CTDirect contactId -> do
contact <- withStore $ \ st -> getContact st userId contactId
sendDirectContactMessage contact $ XFileCancel sharedMsgId
ChatRef CTGroup groupId -> do
Group gInfo ms <- withStore $ \ st -> getGroup st user groupId
sendGroupMessage gInfo ms $ XFileCancel sharedMsgId
_ -> throwChatError $ CEFileInternal " invalid chat ref for file transfer "
ci <- withStore $ \ st -> getChatItemByFileId st user fileId
pure $ CRSndGroupFileCancelled ci ftm fts
FTRcv ftr @ RcvFileTransfer { cancelled } -> do
unless cancelled $ cancelRcvFileTransfer user ftr
pure $ CRRcvFileCancelled ftr
2021-09-04 07:32:56 +01:00
FileStatus fileId ->
2022-05-11 16:18:28 +04:00
CRFileTransferStatus <$> withUser ( \ user -> withStore $ \ st -> getFileTransferProgress st user fileId )
2022-02-06 16:18:01 +00:00
ShowProfile -> withUser $ \ User { profile } -> pure $ CRUserProfile profile
2022-03-10 15:45:40 +04:00
UpdateProfile displayName fullName -> withUser $ \ user @ User { profile } -> do
let p = ( profile :: Profile ) { displayName = displayName , fullName = fullName }
updateProfile user p
UpdateProfileImage image -> withUser $ \ user @ User { profile } -> do
2022-03-19 07:42:54 +00:00
let p = ( profile :: Profile ) { image }
2022-03-10 15:45:40 +04:00
updateProfile user p
2021-07-27 08:08:05 +01:00
QuitChat -> liftIO exitSuccess
2022-02-14 17:51:50 +00:00
ShowVersion -> pure $ CRVersionInfo versionNumber
2021-07-12 19:00:03 +01:00
where
2022-02-06 08:21:40 +00:00
withChatLock action = do
ChatController { chatLock = l , smpAgent = a } <- ask
withAgentLock a . withLock l $ action
2022-02-04 08:02:48 +00:00
-- below code would make command responses asynchronous where they can be slow
-- in View.hs `r'` should be defined as `id` in this case
2022-02-16 16:48:28 +04:00
-- procCmd :: m ChatResponse -> m ChatResponse
2022-02-16 20:31:26 +00:00
-- procCmd action = do
-- ChatController {chatLock = l, smpAgent = a, outputQ = q, idsDrg = gVar} <- ask
-- corrId <- liftIO $ SMP.CorrId <$> randomBytes gVar 8
-- void . forkIO $
-- withAgentLock a . withLock l $
-- (atomically . writeTBQueue q) . (Just corrId,) =<< (action `catchError` (pure . CRChatError))
-- pure $ CRCmdAccepted corrId
-- use function below to make commands "synchronous"
procCmd :: m ChatResponse -> m ChatResponse
procCmd = id
2022-04-28 08:34:21 +01:00
getChatRef :: User -> ChatName -> m ChatRef
getChatRef user @ User { userId } ( ChatName cType name ) =
ChatRef cType <$> case cType of
CTDirect -> withStore $ \ st -> getContactIdByName st userId name
CTGroup -> withStore $ \ st -> getGroupIdByName st user name
_ -> throwChatError $ CECommandError " not supported "
getSentChatItemIdByText :: User -> ChatRef -> ByteString -> m Int64
getSentChatItemIdByText user @ User { userId , localDisplayName } ( ChatRef cType cId ) msg = case cType of
CTDirect -> withStore $ \ st -> getDirectChatItemIdByText st userId cId SMDSnd ( safeDecodeUtf8 msg )
CTGroup -> withStore $ \ st -> getGroupChatItemIdByText st user cId ( Just localDisplayName ) ( safeDecodeUtf8 msg )
2022-04-28 07:26:43 +01:00
_ -> throwChatError $ CECommandError " not supported "
2022-02-13 13:19:24 +04:00
connectViaContact :: UserId -> ConnectionRequestUri 'CMContact -> Profile -> m ChatResponse
connectViaContact userId cReq profile = withChatLock $ do
let cReqHash = ConnReqUriHash . C . sha256Hash $ strEncode cReq
withStore ( \ st -> getConnReqContactXContactId st userId cReqHash ) >>= \ case
( Just contact , _ ) -> pure $ CRContactAlreadyExists contact
( _ , xContactId_ ) -> procCmd $ do
let randomXContactId = XContactId <$> ( asks idsDrg >>= liftIO . ( ` randomBytes ` 16 ) )
xContactId <- maybe randomXContactId pure xContactId_
connId <- withAgent $ \ a -> joinConnection a cReq $ directMessage ( XContact profile $ Just xContactId )
2022-04-23 17:32:40 +01:00
conn <- withStore $ \ st -> createConnReqConnection st userId connId cReqHash xContactId
toView $ CRNewContactConnection conn
2022-02-13 13:19:24 +04:00
pure CRSentInvitation
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
2022-04-15 09:36:38 +04:00
fsFilePath <- toFSFilePath f
unlessM ( doesFileExist fsFilePath ) . throwChatError $ CEFileNotFound f
( , ) <$> getFileSize fsFilePath <*> asks ( fileChunkSize . config )
2022-03-10 15:45:40 +04:00
updateProfile :: User -> Profile -> m ChatResponse
2022-03-29 08:53:30 +01:00
updateProfile user @ User { profile = p } p' @ Profile { displayName }
| p' == p = pure CRUserProfileNoChange
| otherwise = do
withStore $ \ st -> updateUserProfile st user p'
let user' = ( user :: User ) { localDisplayName = displayName , profile = p' }
asks currentUser >>= atomically . ( ` writeTVar ` Just user' )
contacts <- filter isReady <$> withStore ( ` getUserContacts ` user )
withChatLock . procCmd $ do
forM_ contacts $ \ ct ->
void ( sendDirectContactMessage ct $ XInfo p' ) ` catchError ` ( toView . CRChatError )
pure $ CRUserProfileUpdated p p'
isReady :: Contact -> Bool
isReady ct =
let s = connStatus $ activeConn ( ct :: Contact )
in s == ConnReady || s == ConnSndReady
2022-04-15 13:16:34 +01:00
-- perform an action only if filesFolder is set (i.e. on mobile devices)
withFilesFolder :: ( FilePath -> m () ) -> m ()
withFilesFolder action = asks filesFolder >>= readTVarIO >>= mapM_ action
2022-05-17 11:22:09 +04:00
deleteFile :: FilePath -> CIFileInfo -> m ()
deleteFile filesFolder CIFileInfo { filePath } =
forM_ filePath $ \ fPath -> do
let fsFilePath = filesFolder <> " / " <> fPath
removeFile fsFilePath ` E . catch ` \ ( _ :: E . SomeException ) ->
removePathForcibly fsFilePath ` E . catch ` \ ( _ :: E . SomeException ) -> pure ()
cancelFile :: User -> CIFileInfo -> m ()
cancelFile user CIFileInfo { fileId , fileStatus = ( AFS dir status ) } =
2022-05-11 16:18:28 +04:00
unless ( ciFileEnded status ) $
case dir of
SMDSnd -> do
( ftm @ FileTransferMeta { cancelled } , fts ) <- withStore ( \ st -> getSndFileTransfer st user fileId )
unless cancelled $ cancelSndFile user ftm fts
SMDRcv -> do
ft @ RcvFileTransfer { cancelled } <- withStore ( \ st -> getRcvFileTransfer st user fileId )
unless cancelled $ cancelRcvFileTransfer user ft
2022-05-04 13:31:00 +01:00
withCurrentCall :: ContactId -> ( UserId -> Contact -> Call -> m ( Maybe Call ) ) -> m ChatResponse
2022-05-03 10:22:35 +01:00
withCurrentCall ctId action = withUser $ \ User { userId } -> do
ct <- withStore $ \ st -> getContact st userId ctId
2022-05-04 13:31:00 +01:00
calls <- asks currentCalls
2022-05-03 10:22:35 +01:00
withChatLock $
2022-05-04 13:31:00 +01:00
atomically ( TM . lookup ctId calls ) >>= \ case
2022-05-03 10:22:35 +01:00
Nothing -> throwChatError CENoCurrentCall
Just call @ Call { contactId }
| ctId == contactId -> do
call_ <- action userId ct call
2022-05-04 13:31:00 +01:00
atomically $ case call_ of
Just call' -> TM . insert ctId call' calls
_ -> TM . delete ctId calls
2022-05-03 10:22:35 +01:00
pure CRCmdOk
| otherwise -> throwChatError $ CECallContact contactId
2022-04-10 13:30:58 +04:00
2022-05-04 13:31:00 +01:00
updateCallItemStatus :: ChatMonad m => UserId -> Contact -> Call -> WebRTCCallStatus -> Maybe MessageId -> m ()
updateCallItemStatus userId ct Call { chatItemId } receivedStatus msgId_ = do
aciContent_ <- callStatusItemContent userId ct chatItemId receivedStatus
forM_ aciContent_ $ \ aciContent -> updateDirectChatItemView userId ct chatItemId aciContent msgId_
updateDirectChatItemView :: ChatMonad m => UserId -> Contact -> ChatItemId -> ACIContent -> Maybe MessageId -> m ()
updateDirectChatItemView userId ct @ Contact { contactId } chatItemId ( ACIContent msgDir ciContent ) msgId_ = do
updCi <- withStore $ \ st -> updateDirectChatItem st userId contactId chatItemId ciContent msgId_
toView . CRChatItemUpdated $ AChatItem SCTDirect msgDir ( DirectChat ct ) updCi
callStatusItemContent :: ChatMonad m => UserId -> Contact -> ChatItemId -> WebRTCCallStatus -> m ( Maybe ACIContent )
callStatusItemContent userId Contact { contactId } chatItemId receivedStatus = do
CChatItem msgDir ChatItem { meta = CIMeta { updatedAt } , content } <-
withStore $ \ st -> getDirectChatItem st userId contactId chatItemId
ts <- liftIO getCurrentTime
let callDuration :: Int = nominalDiffTimeToSeconds ( ts ` diffUTCTime ` updatedAt ) ` div' ` 1
callStatus = case content of
CISndCall st _ -> Just st
CIRcvCall st _ -> Just st
_ -> Nothing
newState_ = case ( callStatus , receivedStatus ) of
( Just CISCallProgress , WCSConnected ) -> Nothing -- if call in-progress received connected -> no change
( Just CISCallProgress , WCSDisconnected ) -> Just ( CISCallEnded , callDuration ) -- calculate in-progress duration
( Just CISCallProgress , WCSFailed ) -> Just ( CISCallEnded , callDuration ) -- whether call disconnected or failed
( Just CISCallEnded , _ ) -> Nothing -- if call already ended or failed -> no change
( Just CISCallError , _ ) -> Nothing
( Just _ , WCSConnected ) -> Just ( CISCallProgress , 0 ) -- if call ended that was never connected, duration = 0
( Just _ , WCSDisconnected ) -> Just ( CISCallEnded , 0 )
( Just _ , WCSFailed ) -> Just ( CISCallError , 0 )
( Nothing , _ ) -> Nothing -- some other content - we should never get here, but no exception is thrown
pure $ aciContent msgDir <$> newState_
where
aciContent :: forall d . SMsgDirection d -> ( CICallStatus , Int ) -> ACIContent
aciContent msgDir ( callStatus' , duration ) = case msgDir of
SMDSnd -> ACIContent SMDSnd $ CISndCall callStatus' duration
SMDRcv -> ACIContent SMDRcv $ CIRcvCall callStatus' duration
2022-04-15 09:36:38 +04:00
-- mobile clients use file paths relative to app directory (e.g. for the reason ios app directory changes on updates),
-- so we have to differentiate between the file path stored in db and communicated with frontend, and the file path
-- used during file transfer for actual operations with file system
toFSFilePath :: ChatMonad m => FilePath -> m FilePath
2022-04-15 13:16:34 +01:00
toFSFilePath f =
maybe f ( <> " / " <> f ) <$> ( readTVarIO =<< asks filesFolder )
2022-04-15 09:36:38 +04:00
2022-04-29 15:56:56 +04:00
acceptFileReceive :: forall m . ChatMonad m => User -> RcvFileTransfer -> Maybe FilePath -> m AChatItem
2022-05-11 16:18:28 +04:00
acceptFileReceive user @ User { userId } RcvFileTransfer { fileId , fileInvitation = FileInvitation { fileName = fName , fileConnReq } , fileStatus , grpMemberId } filePath_ = do
unless ( fileStatus == RFSNew ) $ case fileStatus of
RFSCancelled _ -> throwChatError $ CEFileCancelled fName
_ -> throwChatError $ CEFileAlreadyReceiving fName
2022-04-10 13:30:58 +04:00
case fileConnReq of
2022-05-11 16:18:28 +04:00
-- direct file protocol
2022-04-10 13:30:58 +04:00
Just connReq ->
tryError ( withAgent $ \ a -> joinConnection a connReq . directMessage $ XFileAcpt fName ) >>= \ case
Right agentConnId -> do
filePath <- getRcvFilePath filePath_ fName
2022-05-12 17:37:09 +04:00
withStore $ \ st -> acceptRcvFileTransfer st user fileId agentConnId ConnJoined filePath
2022-04-10 13:30:58 +04:00
Left e -> throwError e
2022-05-11 16:18:28 +04:00
-- group file protocol
2022-04-10 13:30:58 +04:00
Nothing ->
case grpMemberId of
2022-05-11 16:18:28 +04:00
Nothing -> throwChatError $ CEFileInternal " group member not found for file transfer "
2022-04-10 13:30:58 +04:00
Just memId -> do
( GroupInfo { groupId } , GroupMember { activeConn } ) <- withStore $ \ st -> getGroupAndMember st user memId
case activeConn of
2022-05-11 16:18:28 +04:00
Just conn -> do
sharedMsgId <- withStore $ \ st -> getSharedMsgIdByFileId st userId fileId
( agentConnId , fileInvConnReq ) <- withAgent ( ` createConnection ` SCMInvitation )
filePath <- getRcvFilePath filePath_ fName
2022-05-12 17:37:09 +04:00
ci <- withStore $ \ st -> acceptRcvFileTransfer st user fileId agentConnId ConnNew filePath
2022-05-11 16:18:28 +04:00
void $ sendDirectMessage conn ( XFileAcptInv sharedMsgId fileInvConnReq fName ) ( GroupId groupId )
pure ci
_ -> throwChatError $ CEFileInternal " member connection not active "
2022-04-10 13:30:58 +04:00
where
getRcvFilePath :: Maybe FilePath -> String -> m FilePath
getRcvFilePath fPath_ fn = case fPath_ of
2022-04-15 13:16:34 +01:00
Nothing ->
asks filesFolder >>= readTVarIO >>= \ case
2022-04-15 09:36:38 +04:00
Nothing -> do
dir <- ( ` combine ` " Downloads " ) <$> getHomeDirectory
ifM ( doesDirectoryExist dir ) ( pure dir ) getTemporaryDirectory
>>= ( ` uniqueCombine ` fn )
>>= createEmptyFile
Just filesFolder ->
filesFolder ` uniqueCombine ` fn
>>= createEmptyFile
>>= pure <$> takeFileName
2021-09-04 07:32:56 +01:00
Just fPath ->
ifM
( doesDirectoryExist fPath )
2022-04-10 13:30:58 +04:00
( fPath ` uniqueCombine ` fn >>= createEmptyFile )
2021-09-04 07:32:56 +01:00
$ ifM
( doesFileExist fPath )
2022-01-26 21:20:08 +00:00
( throwChatError $ CEFileAlreadyExists fPath )
2021-09-04 07:32:56 +01:00
( createEmptyFile fPath )
where
createEmptyFile :: FilePath -> m FilePath
2022-01-26 21:20:08 +00:00
createEmptyFile fPath = emptyFile fPath ` E . catch ` ( throwChatError . CEFileWrite fPath . ( show :: E . SomeException -> String ) )
2021-09-04 07:32:56 +01:00
emptyFile :: FilePath -> m FilePath
emptyFile fPath = do
h <- getFileHandle fileId fPath rcvFiles AppendMode
liftIO $ B . hPut h " " >> hFlush h
pure fPath
2022-04-10 13:30:58 +04:00
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
2022-02-14 14:59:11 +04:00
acceptContactRequest :: ChatMonad m => User -> UserContactRequest -> m Contact
acceptContactRequest User { userId , profile } UserContactRequest { agentInvitationId = AgentInvId invId , localDisplayName = cName , profileId , profile = p , xContactId } = do
connId <- withAgent $ \ a -> acceptContact a invId . directMessage $ XInfo profile
withStore $ \ st -> createAcceptedContact st userId connId cName profileId p xContactId
2022-02-06 16:18:01 +00:00
agentSubscriber :: ( MonadUnliftIO m , MonadReader ChatController m ) => User -> m ()
agentSubscriber user = do
2021-07-06 19:07:03 +01:00
q <- asks $ subQ . smpAgent
2021-08-05 20:51:48 +01:00
l <- asks chatLock
2022-04-30 12:47:50 +01:00
subscribeUserConnections subscribeConnection user
2021-06-25 18:18:24 +01:00
forever $ do
2021-07-06 19:07:03 +01:00
( _ , connId , msg ) <- atomically $ readTBQueue q
2022-02-06 16:18:01 +00:00
u <- readTVarIO =<< asks currentUser
2021-08-05 20:51:48 +01:00
withLock l . void . runExceptT $
2022-02-06 16:18:01 +00:00
processAgentMessage u connId msg ` catchError ` ( toView . CRChatError )
2022-04-30 12:47:50 +01:00
subscribeUserConnections ::
( MonadUnliftIO m , MonadReader ChatController m ) =>
( forall m' . ChatMonad m' => AgentClient -> ConnId -> ExceptT AgentErrorType m' () ) ->
User ->
m ()
subscribeUserConnections agentSubscribe user @ User { userId } = do
2022-02-26 10:04:25 +00:00
n <- asks $ subscriptionConcurrency . config
2022-02-25 16:29:36 +04:00
ce <- asks $ subscriptionEvents . config
2022-02-26 10:04:25 +00:00
void . runExceptT $ do
catchErr $ subscribeContacts n ce
catchErr $ subscribeUserContactLink n
catchErr $ subscribeGroups n ce
catchErr $ subscribeFiles n
catchErr $ subscribePendingConnections n
2021-07-25 20:23:52 +01:00
where
2022-02-26 10:04:25 +00:00
catchErr a = a ` catchError ` \ _ -> pure ()
subscribeContacts n ce = do
2021-07-25 20:23:52 +01:00
contacts <- withStore ( ` getUserContacts ` user )
2022-02-26 10:04:25 +00:00
toView . CRContactSubSummary =<< pooledForConcurrentlyN n contacts ( \ ct -> ContactSubStatus ct <$> subscribeContact ce ct )
2022-02-25 16:29:36 +04:00
subscribeContact ce ct =
2022-04-25 09:17:12 +01:00
( subscribe ( contactConnId ct ) $> Nothing )
2022-02-25 16:29:36 +04:00
` catchError ` ( \ e -> when ce ( toView $ CRContactSubError ct e ) $> Just e )
2022-02-26 10:04:25 +00:00
subscribeGroups n ce = do
2021-08-02 20:10:24 +01:00
groups <- withStore ( ` getUserGroups ` user )
2022-02-26 10:04:25 +00:00
toView . CRMemberSubErrors . mconcat =<< forM groups ( subscribeGroup n ce )
subscribeGroup n ce ( Group g @ GroupInfo { membership } members ) = do
2022-02-25 16:29:36 +04:00
let connectedMembers = mapMaybe ( \ m -> ( m , ) <$> memberConnId m ) members
if memberStatus membership == GSMemInvited
then do
toView $ CRGroupInvitation g
pure []
else
if null connectedMembers
then do
if memberActive membership
then toView $ CRGroupEmpty g
else toView $ CRGroupRemoved g
pure []
else do
2022-02-26 10:04:25 +00:00
ms <- pooledForConcurrentlyN n connectedMembers $ \ ( m @ GroupMember { localDisplayName = c } , cId ) ->
2022-02-25 16:29:36 +04:00
( m , ) <$> ( ( subscribe cId $> Nothing ) ` catchError ` ( \ e -> when ce ( toView $ CRMemberSubError g c e ) $> Just e ) )
toView $ CRGroupSubscribed g
2022-03-19 09:04:53 +00:00
pure $ mapMaybe ( \ ( m , e ) -> ( Just . MemberSubError m ) =<< e ) ms
2022-02-26 10:04:25 +00:00
subscribeFiles n = do
2022-02-25 16:29:36 +04:00
sndFileTransfers <- withStore ( ` getLiveSndFileTransfers ` user )
2022-02-26 10:04:25 +00:00
pooledForConcurrentlyN_ n sndFileTransfers $ \ sft -> subscribeSndFile sft
2022-02-25 16:29:36 +04:00
rcvFileTransfers <- withStore ( ` getLiveRcvFileTransfers ` user )
2022-02-26 10:04:25 +00:00
pooledForConcurrentlyN_ n rcvFileTransfers $ \ rft -> subscribeRcvFile rft
2021-09-04 07:32:56 +01:00
where
2022-01-26 16:18:27 +04:00
subscribeSndFile ft @ SndFileTransfer { fileId , fileStatus , agentConnId = AgentConnId cId } = do
subscribe cId ` catchError ` ( toView . CRSndFileSubError ft )
2021-09-04 07:32:56 +01:00
void . forkIO $ do
threadDelay 1000000
l <- asks chatLock
a <- asks smpAgent
2022-04-26 12:52:41 +04:00
when ( fileStatus == FSConnected ) . unlessM ( isFileActive fileId sndFiles ) $
2021-09-04 07:32:56 +01:00
withAgentLock a . withLock l $
2022-05-05 10:37:53 +01:00
sendFileChunk user ft
2021-09-04 07:32:56 +01:00
subscribeRcvFile ft @ RcvFileTransfer { fileStatus } =
case fileStatus of
RFSAccepted fInfo -> resume fInfo
RFSConnected fInfo -> resume fInfo
_ -> pure ()
where
2022-01-26 16:18:27 +04:00
resume RcvFileInfo { agentConnId = AgentConnId cId } =
subscribe cId ` catchError ` ( toView . CRRcvFileSubError ft )
2022-02-26 10:04:25 +00:00
subscribePendingConnections n = do
2021-12-08 13:09:51 +00:00
cs <- withStore ( ` getPendingConnections ` user )
2022-02-26 20:21:32 +00:00
summary <- pooledForConcurrentlyN n cs $ \ Connection { agentConnId = acId @ ( AgentConnId cId ) } ->
PendingSubStatus acId <$> ( ( subscribe cId $> Nothing ) ` catchError ` ( pure . Just ) )
toView $ CRPendingSubSummary summary
2022-02-26 10:04:25 +00:00
subscribeUserContactLink n = do
2021-12-08 13:09:51 +00:00
cs <- withStore ( ` getUserContactLinkConnections ` userId )
2022-02-26 10:04:25 +00:00
( subscribeConns n cs >> toView CRUserContactLinkSubscribed )
2022-01-24 16:07:17 +00:00
` catchError ` ( toView . CRUserContactLinkSubError )
2022-04-30 12:47:50 +01:00
subscribe cId = withAgent ( ` agentSubscribe ` cId )
2022-02-26 10:04:25 +00:00
subscribeConns n conns =
2021-12-08 13:09:51 +00:00
withAgent $ \ a ->
2022-04-30 12:47:50 +01:00
pooledForConcurrentlyN_ n conns $ \ c -> agentSubscribe a ( aConnId c )
2021-07-25 20:23:52 +01:00
2022-02-06 16:18:01 +00:00
processAgentMessage :: forall m . ChatMonad m => Maybe User -> ConnId -> ACommand 'Agent -> m ()
processAgentMessage Nothing _ _ = throwChatError CENoActiveUser
2022-04-25 09:17:12 +01:00
processAgentMessage ( Just User { userId } ) " " agentMessage = case agentMessage of
DOWN srv conns -> serverEvent srv conns CRContactsDisconnected " disconnected "
UP srv conns -> serverEvent srv conns CRContactsSubscribed " connected "
_ -> pure ()
where
serverEvent srv @ SMP . ProtocolServer { host , port } conns event str = do
cs <- withStore $ \ st -> getConnectionsContacts st userId conns
toView $ event srv cs
showToast ( " server " <> str ) ( safeDecodeUtf8 . strEncode $ SrvLoc host port )
2022-02-06 16:18:01 +00:00
processAgentMessage ( Just user @ User { userId , profile } ) agentConnId agentMessage =
2022-02-02 11:31:01 +00:00
( withStore ( \ st -> getConnectionEntity st user agentConnId ) >>= updateConnStatus ) >>= \ case
2022-01-26 16:18:27 +04:00
RcvDirectMsgConnection conn contact_ ->
processDirectMessage agentMessage conn contact_
RcvGroupMsgConnection conn gInfo m ->
processGroupMessage agentMessage conn gInfo m
2021-09-04 07:32:56 +01:00
RcvFileConnection conn ft ->
processRcvFileConn agentMessage conn ft
SndFileConnection conn ft ->
processSndFileConn agentMessage conn ft
2021-12-08 13:09:51 +00:00
UserContactConnection conn uc ->
processUserContactRequest agentMessage conn uc
2021-07-24 18:11:04 +01:00
where
2022-02-02 11:31:01 +00:00
updateConnStatus :: ConnectionEntity -> m ConnectionEntity
updateConnStatus acEntity = case agentMsgConnStatus agentMessage of
Just connStatus -> do
let conn = ( entityConnection acEntity ) { connStatus }
withStore $ \ st -> updateConnectionStatus st conn connStatus
2022-02-02 17:01:12 +00:00
pure $ updateEntityConnStatus acEntity connStatus
2022-02-02 11:31:01 +00:00
Nothing -> pure acEntity
2022-01-26 16:18:27 +04:00
isMember :: MemberId -> GroupInfo -> [ GroupMember ] -> Bool
isMember memId GroupInfo { membership } members =
2022-01-11 08:50:44 +00:00
sameMemberId memId membership || isJust ( find ( sameMemberId memId ) members )
2021-07-24 18:11:04 +01:00
agentMsgConnStatus :: ACommand 'Agent -> Maybe ConnStatus
agentMsgConnStatus = \ case
2021-12-08 13:09:51 +00:00
CONF { } -> Just ConnRequested
2021-07-24 18:11:04 +01:00
INFO _ -> Just ConnSndReady
CON -> Just ConnReady
_ -> Nothing
processDirectMessage :: ACommand 'Agent -> Connection -> Maybe Contact -> m ()
2022-02-07 15:19:34 +04:00
processDirectMessage agentMsg conn @ Connection { connId } = \ case
2021-07-24 18:11:04 +01:00
Nothing -> case agentMsg of
2021-12-08 13:09:51 +00:00
CONF confId connInfo -> do
2021-07-24 18:11:04 +01:00
saveConnInfo conn connInfo
2021-12-08 13:09:51 +00:00
allowAgentConnection conn confId $ XInfo profile
2021-07-24 18:11:04 +01:00
INFO connInfo ->
saveConnInfo conn connInfo
2021-12-29 23:11:55 +04:00
MSG meta msgBody -> do
2022-03-13 19:34:03 +00:00
_ <- saveRcvMSG conn ( ConnectionId connId ) meta msgBody
2021-09-04 07:32:56 +01:00
withAckMessage agentConnId meta $ pure ()
2021-12-29 23:11:55 +04:00
ackMsgDeliveryEvent conn meta
SENT msgId ->
2022-03-23 11:37:51 +00:00
-- ? updateDirectChatItemStatus
2021-12-29 23:11:55 +04:00
sentMsgDeliveryEvent conn msgId
2022-01-12 11:54:40 +00:00
-- TODO print errors
2022-03-23 11:37:51 +00:00
MERR _ _ -> pure () -- ? updateDirectChatItemStatus
2022-01-12 11:54:40 +00:00
ERR _ -> pure ()
-- TODO add debugging output
2021-08-14 21:04:51 +01:00
_ -> pure ()
2022-03-13 19:34:03 +00:00
Just ct @ Contact { localDisplayName = c , contactId } -> case agentMsg of
2022-01-26 16:18:27 +04:00
MSG msgMeta msgBody -> do
2022-03-16 13:20:47 +00:00
msg @ RcvMessage { chatMsgEvent } <- saveRcvMSG conn ( ConnectionId connId ) msgMeta msgBody
2022-01-26 16:18:27 +04:00
withAckMessage agentConnId msgMeta $
2021-12-29 23:11:55 +04:00
case chatMsgEvent of
2022-03-13 19:34:03 +00:00
XMsgNew mc -> newContentMessage ct mc msg msgMeta
2022-03-23 11:37:51 +00:00
XMsgUpdate sharedMsgId mContent -> messageUpdate ct sharedMsgId mContent msg msgMeta
2022-03-28 20:35:57 +04:00
XMsgDel sharedMsgId -> messageDelete ct sharedMsgId msg msgMeta
2022-04-10 13:30:58 +04:00
-- TODO discontinue XFile
XFile fInv -> processFileInvitation' ct fInv msg msgMeta
2022-05-11 16:18:28 +04:00
XFileCancel sharedMsgId -> xFileCancel ct sharedMsgId msgMeta
2021-12-29 23:11:55 +04:00
XInfo p -> xInfo ct p
XGrpInv gInv -> processGroupInvitation ct gInv
XInfoProbe probe -> xInfoProbe ct probe
XInfoProbeCheck probeHash -> xInfoProbeCheck ct probeHash
XInfoProbeOk probe -> xInfoProbeOk ct probe
2022-05-03 10:22:35 +01:00
XCallInv callId invitation -> xCallInv ct callId invitation msg msgMeta
XCallOffer callId offer -> xCallOffer ct callId offer msg msgMeta
XCallAnswer callId answer -> xCallAnswer ct callId answer msg msgMeta
XCallExtra callId extraInfo -> xCallExtra ct callId extraInfo msg msgMeta
XCallEnd callId -> xCallEnd ct callId msg msgMeta
2021-12-29 23:11:55 +04:00
_ -> pure ()
2022-01-26 16:18:27 +04:00
ackMsgDeliveryEvent conn msgMeta
2021-12-08 13:09:51 +00:00
CONF 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-12-08 13:09:51 +00:00
allowAgentConnection conn confId XOk
_ -> messageError " CONF 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 ()
2021-12-08 13:09:51 +00:00
XInfo _profile -> do
-- TODO update contact profile
pure ()
2021-07-24 10:26:28 +01:00
XOk -> pure ()
2021-12-13 12:05:57 +00:00
_ -> messageError " INFO for existing contact must have x.grp.mem.info, x.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
2022-01-24 16:07:17 +00:00
toView $ CRContactConnected ct
2021-07-24 10:26:28 +01:00
setActive $ ActiveC c
showToast ( c <> " > " ) " connected "
2022-02-14 18:49:42 +04:00
Just ( gInfo , m @ GroupMember { activeConn } ) -> do
when ( maybe False ( ( == ConnReady ) . connStatus ) activeConn ) $ do
2022-01-26 16:18:27 +04:00
notifyMemberConnected gInfo m
2021-07-27 08:08:05 +01:00
when ( memberCategory m == GCPreMember ) $ probeMatchingContacts ct
2022-02-07 15:19:34 +04:00
SENT msgId -> do
2021-12-29 23:11:55 +04:00
sentMsgDeliveryEvent conn msgId
2022-05-04 13:31:00 +01:00
withStore ( \ st -> getDirectChatItemByAgentMsgId st userId contactId connId msgId ) >>= \ case
Just ( CChatItem SMDSnd ci ) -> do
chatItem <- withStore $ \ st -> updateDirectChatItemStatus st userId contactId ( chatItemId' ci ) CISSndSent
2022-03-23 11:37:51 +00:00
toView $ CRChatItemStatusUpdated ( AChatItem SCTDirect SMDSnd ( DirectChat ct ) chatItem )
2022-05-04 13:31:00 +01:00
_ -> pure ()
2021-07-06 19:07:03 +01:00
END -> do
2022-01-26 16:18:27 +04:00
toView $ CRContactAnotherClient ct
2021-08-14 21:04:51 +01:00
showToast ( c <> " > " ) " connected to another client "
unsetActive $ ActiveC c
2022-01-12 11:54:40 +00:00
-- TODO print errors
2022-02-07 15:19:34 +04:00
MERR msgId err -> do
chatItemId_ <- withStore $ \ st -> getChatItemIdByAgentMsgId st connId msgId
case chatItemId_ of
Nothing -> pure ()
Just chatItemId -> do
2022-03-23 11:37:51 +00:00
chatItem <- withStore $ \ st -> updateDirectChatItemStatus st userId contactId chatItemId ( agentErrToItemStatus err )
toView $ CRChatItemStatusUpdated ( AChatItem SCTDirect SMDSnd ( DirectChat ct ) chatItem )
2022-01-12 11:54:40 +00:00
ERR _ -> pure ()
-- TODO add debugging output
2021-08-14 21:04:51 +01:00
_ -> pure ()
2021-07-24 10:26:28 +01:00
2022-01-26 16:18:27 +04:00
processGroupMessage :: ACommand 'Agent -> Connection -> GroupInfo -> GroupMember -> m ()
2022-02-25 21:59:35 +04:00
processGroupMessage agentMsg conn gInfo @ GroupInfo { groupId , localDisplayName = gName , membership } m = case agentMsg of
2021-12-08 13:09:51 +00:00
CONF 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
2022-01-11 08:50:44 +00:00
| sameMemberId memId m -> do
2021-08-02 20:10:24 +01:00
withStore $ \ st -> updateGroupMemberStatus st userId m GSMemAccepted
2021-12-08 13:09:51 +00:00
allowAgentConnection conn confId XOk
2021-07-24 18:11:04 +01:00
| otherwise -> messageError " x.grp.acpt: memberId is different from expected "
2021-12-08 13:09:51 +00:00
_ -> messageError " CONF from invited member must have x.grp.acpt "
2021-07-24 18:11:04 +01:00
_ ->
case chatMsgEvent of
XGrpMemInfo memId _memProfile
2022-01-11 08:50:44 +00:00
| sameMemberId memId m -> do
2021-07-24 18:11:04 +01:00
-- TODO update member profile
2022-01-11 08:50:44 +00:00
allowAgentConnection conn confId $ XGrpMemInfo ( memberId ( membership :: GroupMember ) ) profile
2021-07-24 18:11:04 +01:00
| otherwise -> messageError " x.grp.mem.info: memberId is different from expected "
2021-12-08 13:09:51 +00:00
_ -> messageError " CONF 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
2022-01-11 08:50:44 +00:00
| sameMemberId memId m -> do
2021-07-24 18:11:04 +01:00
-- 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
2022-01-26 16:18:27 +04:00
members <- withStore $ \ st -> getGroupMembers st user gInfo
2021-07-24 18:11:04 +01:00
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
2022-01-24 16:07:17 +00:00
sendPendingGroupMessages m conn
2021-07-24 18:11:04 +01:00
case memberCategory m of
GCHostMember -> do
2022-01-26 16:18:27 +04:00
toView $ CRUserJoinedGroup gInfo
2021-07-24 18:11:04 +01:00
setActive $ ActiveG gName
showToast ( " # " <> gName ) " you are connected to group "
GCInviteeMember -> do
2022-01-26 16:18:27 +04:00
toView $ CRJoinedGroupMember gInfo m
2021-07-24 18:11:04 +01:00
setActive $ ActiveG gName
showToast ( " # " <> gName ) $ " member " <> localDisplayName ( m :: GroupMember ) <> " is connected "
2022-01-26 16:18:27 +04:00
intros <- withStore $ \ st -> createIntroductions st members m
2022-02-25 21:59:35 +04:00
void . sendGroupMessage gInfo members . XGrpMemNew $ memberInfo m
2022-01-24 16:07:17 +00:00
forM_ intros $ \ intro @ GroupMemberIntro { introId } -> do
2022-02-25 21:59:35 +04:00
void $ sendDirectMessage conn ( XGrpMemIntro . memberInfo $ reMember intro ) ( GroupId groupId )
2022-01-24 16:07:17 +00:00
withStore $ \ st -> updateIntroStatus st introId GMIntroSent
2021-07-24 18:11:04 +01:00
_ -> 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
2022-01-26 16:18:27 +04:00
notifyMemberConnected gInfo m
2021-07-24 18:11:04 +01:00
messageError " implementation error: connected member does not have contact "
2022-02-14 18:49:42 +04:00
Just ct @ Contact { activeConn = Connection { connStatus } } ->
when ( connStatus == ConnReady ) $ do
2022-01-26 16:18:27 +04:00
notifyMemberConnected gInfo m
2021-07-27 08:08:05 +01:00
when ( memberCategory m == GCPreMember ) $ probeMatchingContacts ct
2022-01-26 16:18:27 +04:00
MSG msgMeta msgBody -> do
2022-03-16 13:20:47 +00:00
msg @ RcvMessage { chatMsgEvent } <- saveRcvMSG conn ( GroupId groupId ) msgMeta msgBody
2022-01-26 16:18:27 +04:00
withAckMessage agentConnId msgMeta $
2021-12-29 23:11:55 +04:00
case chatMsgEvent of
2022-03-13 19:34:03 +00:00
XMsgNew mc -> newGroupContentMessage gInfo m mc msg msgMeta
2022-03-28 20:35:57 +04:00
XMsgUpdate sharedMsgId mContent -> groupMessageUpdate gInfo m sharedMsgId mContent msg
XMsgDel sharedMsgId -> groupMessageDelete gInfo m sharedMsgId msg
2022-04-10 13:30:58 +04:00
-- TODO discontinue XFile
XFile fInv -> processGroupFileInvitation' gInfo m fInv msg msgMeta
2022-05-11 16:18:28 +04:00
XFileCancel sharedMsgId -> xFileCancelGroup gInfo m sharedMsgId msgMeta
2022-04-05 10:01:08 +04:00
XFileAcptInv sharedMsgId fileConnReq fName -> xFileAcptInvGroup gInfo m sharedMsgId fileConnReq fName msgMeta
2022-01-26 16:18:27 +04:00
XGrpMemNew memInfo -> xGrpMemNew gInfo m memInfo
XGrpMemIntro memInfo -> xGrpMemIntro conn gInfo m memInfo
XGrpMemInv memId introInv -> xGrpMemInv gInfo m memId introInv
XGrpMemFwd memInfo introInv -> xGrpMemFwd gInfo m memInfo introInv
XGrpMemDel memId -> xGrpMemDel gInfo m memId
XGrpLeave -> xGrpLeave gInfo m
XGrpDel -> xGrpDel gInfo m
2021-12-29 23:11:55 +04:00
_ -> messageError $ " unsupported message: " <> T . pack ( show chatMsgEvent )
2022-01-26 16:18:27 +04:00
ackMsgDeliveryEvent conn msgMeta
2021-12-29 23:11:55 +04:00
SENT msgId ->
sentMsgDeliveryEvent conn msgId
2022-01-12 11:54:40 +00:00
-- TODO print errors
MERR _ _ -> pure ()
ERR _ -> pure ()
-- TODO add debugging output
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
2022-05-11 16:18:28 +04:00
-- SMP CONF for SndFileConnection happens for direct file protocol
-- when recipient of the file "joins" connection created by the sender
2021-12-08 13:09:51 +00:00
CONF confId connInfo -> do
2021-09-04 07:32:56 +01:00
ChatMessage { chatMsgEvent } <- liftEither $ parseChatMessage connInfo
case chatMsgEvent of
2021-12-29 23:11:55 +04:00
-- TODO save XFileAcpt message
2021-09-04 07:32:56 +01:00
XFileAcpt name
| name == fileName -> do
withStore $ \ st -> updateSndFileStatus st ft FSAccepted
2021-12-08 13:09:51 +00:00
allowAgentConnection conn confId XOk
2021-09-04 07:32:56 +01:00
| otherwise -> messageError " x.file.acpt: fileName is different from expected "
2021-12-08 13:09:51 +00:00
_ -> messageError " CONF from file connection must have x.file.acpt "
2021-09-04 07:32:56 +01:00
CON -> do
2022-05-05 10:37:53 +01:00
ci <- withStore $ \ st -> do
updateSndFileStatus st ft FSConnected
2022-05-05 13:50:19 +01:00
updateDirectCIFileStatus st user fileId CIFSSndTransfer
2022-05-05 10:37:53 +01:00
toView $ CRSndFileStart ci ft
sendFileChunk user ft
2021-09-04 07:32:56 +01:00
SENT msgId -> do
withStore $ \ st -> updateSndFileChunkSent st ft msgId
2022-05-05 10:37:53 +01:00
unless ( fileStatus == FSCancelled ) $ sendFileChunk user ft
2021-09-04 07:32:56 +01:00
MERR _ err -> do
cancelSndFileTransfer ft
case err of
2022-05-05 10:37:53 +01:00
SMP SMP . AUTH -> unless ( fileStatus == FSCancelled ) $ do
ci <- withStore $ \ st -> getChatItemByFileId st user fileId
toView $ CRSndFileRcvCancelled ci ft
2022-01-26 21:20:08 +00:00
_ -> throwChatError $ CEFileSend fileId err
2021-09-04 07:32:56 +01:00
MSG meta _ ->
withAckMessage agentConnId meta $ pure ()
2022-01-12 11:54:40 +00:00
-- TODO print errors
ERR _ -> pure ()
-- TODO add debugging output
2021-09-04 07:32:56 +01:00
_ -> pure ()
processRcvFileConn :: ACommand 'Agent -> Connection -> RcvFileTransfer -> m ()
2022-05-11 16:18:28 +04:00
processRcvFileConn agentMsg conn ft @ RcvFileTransfer { fileId , chunkSize , cancelled } =
2021-09-04 07:32:56 +01:00
case agentMsg of
2022-05-11 16:18:28 +04:00
-- SMP CONF for RcvFileConnection happens for group file protocol
-- when sender of the file "joins" connection created by the recipient
-- (sender doesn't create connections for all group members)
2022-04-05 10:01:08 +04:00
CONF confId connInfo -> do
ChatMessage { chatMsgEvent } <- liftEither $ parseChatMessage connInfo
case chatMsgEvent of
XOk -> allowAgentConnection conn confId XOk
_ -> pure ()
2021-09-04 07:32:56 +01:00
CON -> do
2022-04-29 15:56:56 +04:00
ci <- withStore $ \ st -> do
updateRcvFileStatus st ft FSConnected
2022-05-11 16:18:28 +04:00
updateCIFileStatus st user fileId CIFSRcvTransfer
2022-04-29 15:56:56 +04:00
getChatItemByFileId st user fileId
toView $ CRRcvFileStart ci
2021-09-04 07:32:56 +01:00
MSG meta @ MsgMeta { recipient = ( msgId , _ ) , integrity } msgBody -> withAckMessage agentConnId meta $ do
parseFileChunk msgBody >>= \ case
2022-05-11 16:18:28 +04:00
FileChunkCancel ->
unless cancelled $ do
cancelRcvFileTransfer user ft
toView ( CRRcvFileSndCancelled ft )
2022-01-11 12:41:38 +00:00
FileChunk { chunkNo , chunkBytes = chunk } -> do
2021-09-04 07:32:56 +01:00
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
2022-04-15 09:36:38 +04:00
ci <- withStore $ \ st -> do
2021-09-05 14:08:29 +01:00
updateRcvFileStatus st ft FSComplete
2022-05-11 16:18:28 +04:00
updateCIFileStatus st user fileId CIFSRcvComplete
2021-09-05 14:08:29 +01:00
deleteRcvFileChunks st ft
2022-04-15 09:36:38 +04:00
getChatItemByFileId st user fileId
toView $ CRRcvFileComplete ci
2021-09-04 07:32:56 +01:00
closeFileHandle fileId rcvFiles
withAgent ( ` deleteConnection ` agentConnId )
RcvChunkDuplicate -> pure ()
RcvChunkError -> badRcvFileChunk ft $ " incorrect chunk number " <> show chunkNo
2022-01-12 11:54:40 +00:00
-- TODO print errors
MERR _ _ -> pure ()
ERR _ -> pure ()
-- TODO add debugging output
2021-09-04 07:32:56 +01:00
_ -> pure ()
2021-12-08 13:09:51 +00:00
processUserContactRequest :: ACommand 'Agent -> Connection -> UserContact -> m ()
processUserContactRequest agentMsg _conn UserContact { userContactLinkId } = case agentMsg of
REQ invId connInfo -> do
ChatMessage { chatMsgEvent } <- liftEither $ parseChatMessage connInfo
case chatMsgEvent of
2022-02-13 13:19:24 +04:00
XContact p xContactId_ -> profileContactRequest invId p xContactId_
XInfo p -> profileContactRequest invId p Nothing
2021-12-08 13:09:51 +00:00
-- TODO show/log error, other events in contact request
_ -> pure ()
2022-01-12 11:54:40 +00:00
-- TODO print errors
MERR _ _ -> pure ()
ERR _ -> pure ()
-- TODO add debugging output
2021-12-08 13:09:51 +00:00
_ -> pure ()
where
2022-02-13 13:19:24 +04:00
profileContactRequest :: InvitationId -> Profile -> Maybe XContactId -> m ()
profileContactRequest invId p xContactId_ = do
withStore ( \ st -> createOrUpdateContactRequest st userId userContactLinkId invId p xContactId_ ) >>= \ case
2022-05-14 00:57:24 +04:00
CORContact contact -> toView $ CRContactRequestAlreadyAccepted contact
CORRequest cReq @ UserContactRequest { localDisplayName } -> do
2022-02-14 14:59:11 +04:00
( _ , autoAccept ) <- withStore $ \ st -> getUserContactLink st userId
if autoAccept
then acceptContactRequest user cReq >>= toView . CRAcceptingContactRequest
else do
toView $ CRReceivedContactRequest cReq
showToast ( localDisplayName <> " > " ) " wants to connect to you "
2021-12-08 13:09:51 +00:00
2021-09-04 07:32:56 +01:00
withAckMessage :: ConnId -> MsgMeta -> m () -> m ()
withAckMessage cId MsgMeta { recipient = ( msgId , _ ) } action =
action ` E . finally ` withAgent ( \ a -> ackMessage a cId msgId ` catchError ` \ _ -> pure () )
2021-12-29 23:11:55 +04:00
ackMsgDeliveryEvent :: Connection -> MsgMeta -> m ()
ackMsgDeliveryEvent Connection { connId } MsgMeta { recipient = ( msgId , _ ) } =
withStore $ \ st -> createRcvMsgDeliveryEvent st connId msgId MDSRcvAcknowledged
sentMsgDeliveryEvent :: Connection -> AgentMsgId -> m ()
sentMsgDeliveryEvent Connection { connId } msgId =
withStore $ \ st -> createSndMsgDeliveryEvent st connId msgId MDSSndSent
2022-02-07 15:19:34 +04:00
agentErrToItemStatus :: AgentErrorType -> CIStatus 'MDSnd
agentErrToItemStatus ( SMP AUTH ) = CISSndErrorAuth
agentErrToItemStatus err = CISSndError err
2021-09-04 07:32:56 +01:00
badRcvFileChunk :: RcvFileTransfer -> String -> m ()
2022-05-11 16:18:28 +04:00
badRcvFileChunk ft @ RcvFileTransfer { cancelled } err =
unless cancelled $ do
cancelRcvFileTransfer user ft
throwChatError $ CEFileRcvChunk err
2021-09-04 07:32:56 +01:00
2022-01-26 16:18:27 +04:00
notifyMemberConnected :: GroupInfo -> GroupMember -> m ()
notifyMemberConnected gInfo m @ GroupMember { localDisplayName = c } = do
toView $ CRConnectedToGroupMember gInfo m
2022-01-27 22:01:15 +00:00
let g = groupName' gInfo
2022-01-26 16:18:27 +04:00
setActive $ ActiveG g
showToast ( " # " <> g ) $ " member " <> c <> " is connected "
2021-07-24 10:26:28 +01:00
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
2022-02-14 18:49:42 +04:00
void . sendDirectContactMessage ct $ XInfoProbe probe
2021-07-27 08:08:05 +01:00
cs <- withStore ( \ st -> getMatchingContacts st userId ct )
2022-01-11 08:50:44 +00:00
let probeHash = ProbeHash $ C . sha256Hash ( unProbe probe )
2021-07-27 08:08:05 +01:00
forM_ cs $ \ c -> sendProbeHash c probeHash probeId ` catchError ` const ( pure () )
where
2022-01-24 16:07:17 +00:00
sendProbeHash :: Contact -> ProbeHash -> Int64 -> m ()
2021-07-27 08:08:05 +01:00
sendProbeHash c probeHash probeId = do
2022-02-14 18:49:42 +04:00
void . sendDirectContactMessage c $ XInfoProbeCheck probeHash
2021-07-27 08:08:05 +01:00
withStore $ \ st -> createSentProbeHash st userId probeId c
2021-07-24 10:26:28 +01:00
messageWarning :: Text -> m ()
2022-01-24 16:07:17 +00:00
messageWarning = toView . CRMessageError " warning "
2021-07-24 10:26:28 +01:00
messageError :: Text -> m ()
2022-01-24 16:07:17 +00:00
messageError = toView . CRMessageError " error "
2021-07-24 10:26:28 +01:00
2022-03-16 13:20:47 +00:00
newContentMessage :: Contact -> MsgContainer -> RcvMessage -> MsgMeta -> m ()
2022-03-13 19:34:03 +00:00
newContentMessage ct @ Contact { localDisplayName = c } mc msg msgMeta = do
2022-05-03 10:22:35 +01:00
checkIntegrity msgMeta $ toView . CRMsgIntegrityError
2022-04-10 13:30:58 +04:00
let ( ExtMsgContent content fileInvitation_ ) = mcExtMsgContent mc
ciFile_ <- processFileInvitation fileInvitation_ $
\ fi chSize -> withStore $ \ st -> createRcvFileTransfer st userId ct fi chSize
ci @ ChatItem { formattedText } <- saveRcvChatItem user ( CDDirectRcv ct ) msg msgMeta ( CIRcvMsgContent content ) ciFile_
2022-01-26 16:18:27 +04:00
toView . CRNewChatItem $ AChatItem SCTDirect SMDRcv ( DirectChat ct ) ci
2022-03-13 20:13:47 +00:00
showMsgToast ( c <> " > " ) content formattedText
2022-01-11 08:50:44 +00:00
setActive $ ActiveC c
2022-04-10 13:30:58 +04:00
processFileInvitation :: Maybe FileInvitation -> ( FileInvitation -> Integer -> m RcvFileTransfer ) -> m ( Maybe ( CIFile 'MDRcv ) )
processFileInvitation fileInvitation_ createRcvFileTransferF = case fileInvitation_ of
Nothing -> pure Nothing
Just fileInvitation @ FileInvitation { fileName , fileSize } -> do
chSize <- asks $ fileChunkSize . config
RcvFileTransfer { fileId } <- createRcvFileTransferF fileInvitation chSize
let ciFile = CIFile { fileId , fileName , fileSize , filePath = Nothing , fileStatus = CIFSRcvInvitation }
pure $ Just ciFile
2022-03-23 11:37:51 +00:00
messageUpdate :: Contact -> SharedMsgId -> MsgContent -> RcvMessage -> MsgMeta -> m ()
2022-05-17 11:22:09 +04:00
messageUpdate ct @ Contact { contactId , localDisplayName = c } sharedMsgId mc msg @ RcvMessage { msgId } msgMeta = do
2022-05-03 10:22:35 +01:00
checkIntegrity msgMeta $ toView . CRMsgIntegrityError
2022-05-17 11:22:09 +04:00
updateRcvChatItem ` catchError ` \ e ->
case e of
( ChatErrorStore ( SEChatItemSharedMsgIdNotFound _ ) ) -> do
-- This patches initial sharedMsgId into chat item when locally deleted chat item
-- received an update from the sender, so that it can be referenced later (e.g. by broadcast delete).
-- Chat item and update message which created it will have different sharedMsgId in this case...
ci @ ChatItem { formattedText } <- saveRcvChatItem' user ( CDDirectRcv ct ) msg ( Just sharedMsgId ) msgMeta ( CIRcvMsgContent mc ) Nothing
toView . CRChatItemUpdated $ AChatItem SCTDirect SMDRcv ( DirectChat ct ) ci
showMsgToast ( c <> " > " ) mc formattedText
setActive $ ActiveC c
_ -> throwError e
where
updateRcvChatItem = do
CChatItem msgDir ChatItem { meta = CIMeta { itemId } } <- withStore $ \ st -> getDirectChatItemBySharedMsgId st userId contactId sharedMsgId
case msgDir of
SMDRcv -> updateDirectChatItemView userId ct itemId ( ACIContent SMDRcv $ CIRcvMsgContent mc ) $ Just msgId
SMDSnd -> messageError " x.msg.update: contact attempted invalid message update "
2022-03-28 20:35:57 +04:00
messageDelete :: Contact -> SharedMsgId -> RcvMessage -> MsgMeta -> m ()
messageDelete ct @ Contact { contactId } sharedMsgId RcvMessage { msgId } msgMeta = do
2022-05-03 10:22:35 +01:00
checkIntegrity msgMeta $ toView . CRMsgIntegrityError
2022-05-17 11:22:09 +04:00
deleteRcvChatItem ` catchError ` \ e ->
case e of
( ChatErrorStore ( SEChatItemSharedMsgIdNotFound sMsgId ) ) -> toView $ CRChatItemDeletedNotFound ct sMsgId
_ -> throwError e
where
deleteRcvChatItem = do
CChatItem msgDir deletedItem @ ChatItem { meta = CIMeta { itemId } } <- withStore $ \ st -> getDirectChatItemBySharedMsgId st userId contactId sharedMsgId
case msgDir of
SMDRcv -> do
toCi <- withStore $ \ st -> deleteDirectChatItemRcvBroadcast st userId ct itemId msgId
toView $ CRChatItemDeleted ( AChatItem SCTDirect SMDRcv ( DirectChat ct ) deletedItem ) toCi
SMDSnd -> messageError " x.msg.del: contact attempted invalid message delete "
2022-03-23 11:37:51 +00:00
2022-03-16 13:20:47 +00:00
newGroupContentMessage :: GroupInfo -> GroupMember -> MsgContainer -> RcvMessage -> MsgMeta -> m ()
2022-03-13 19:34:03 +00:00
newGroupContentMessage gInfo m @ GroupMember { localDisplayName = c } mc msg msgMeta = do
2022-04-10 13:30:58 +04:00
let ( ExtMsgContent content fileInvitation_ ) = mcExtMsgContent mc
ciFile_ <- processFileInvitation fileInvitation_ $
\ fi chSize -> withStore $ \ st -> createRcvGroupFileTransfer st userId m fi chSize
ci @ ChatItem { formattedText } <- saveRcvChatItem user ( CDGroupRcv gInfo m ) msg msgMeta ( CIRcvMsgContent content ) ciFile_
2022-02-22 14:05:45 +00:00
groupMsgToView gInfo ci msgMeta
2022-01-27 22:01:15 +00:00
let g = groupName' gInfo
2022-03-13 20:13:47 +00:00
showMsgToast ( " # " <> g <> " " <> c <> " > " ) content formattedText
2022-01-26 16:18:27 +04:00
setActive $ ActiveG g
2021-07-07 22:46:38 +01:00
2022-03-28 20:35:57 +04:00
groupMessageUpdate :: GroupInfo -> GroupMember -> SharedMsgId -> MsgContent -> RcvMessage -> m ()
groupMessageUpdate gInfo @ GroupInfo { groupId } GroupMember { memberId } sharedMsgId mc RcvMessage { msgId } = do
CChatItem msgDir ChatItem { chatDir , meta = CIMeta { itemId } } <- withStore $ \ st -> getGroupChatItemBySharedMsgId st user groupId sharedMsgId
case ( msgDir , chatDir ) of
( SMDRcv , CIGroupRcv m ) ->
if sameMemberId memberId m
then do
updCi <- withStore $ \ st -> updateGroupChatItem st user groupId itemId ( CIRcvMsgContent mc ) msgId
toView . CRChatItemUpdated $ AChatItem SCTGroup SMDRcv ( GroupChat gInfo ) updCi
else messageError " x.msg.update: group member attempted to update a message of another member "
( SMDSnd , _ ) -> messageError " x.msg.update: group member attempted invalid message update "
groupMessageDelete :: GroupInfo -> GroupMember -> SharedMsgId -> RcvMessage -> m ()
groupMessageDelete gInfo @ GroupInfo { groupId } GroupMember { memberId } sharedMsgId RcvMessage { msgId } = do
CChatItem msgDir deletedItem @ ChatItem { chatDir , meta = CIMeta { itemId } } <- withStore $ \ st -> getGroupChatItemBySharedMsgId st user groupId sharedMsgId
case ( msgDir , chatDir ) of
( SMDRcv , CIGroupRcv m ) ->
if sameMemberId memberId m
then do
toCi <- withStore $ \ st -> deleteGroupChatItemRcvBroadcast st user gInfo itemId msgId
toView $ CRChatItemDeleted ( AChatItem SCTGroup SMDRcv ( GroupChat gInfo ) deletedItem ) toCi
else messageError " x.msg.del: group member attempted to delete a message of another member "
( SMDSnd , _ ) -> messageError " x.msg.del: group member attempted invalid message delete "
2022-03-23 11:37:51 +00:00
2022-04-10 13:30:58 +04:00
-- TODO remove once XFile is discontinued
processFileInvitation' :: Contact -> FileInvitation -> RcvMessage -> MsgMeta -> m ()
processFileInvitation' ct @ Contact { localDisplayName = c } fInv @ FileInvitation { fileName , fileSize } msg msgMeta = do
2022-05-03 10:22:35 +01:00
checkIntegrity msgMeta $ toView . CRMsgIntegrityError
2021-09-04 07:32:56 +01:00
-- TODO chunk size has to be sent as part of invitation
chSize <- asks $ fileChunkSize . config
2022-04-10 13:30:58 +04:00
RcvFileTransfer { fileId } <- withStore $ \ st -> createRcvFileTransfer st userId ct fInv chSize
let ciFile = Just $ CIFile { fileId , fileName , fileSize , filePath = Nothing , fileStatus = CIFSRcvInvitation }
2022-04-30 19:18:46 +04:00
ci <- saveRcvChatItem user ( CDDirectRcv ct ) msg msgMeta ( CIRcvMsgContent $ MCFile " " ) ciFile
2022-01-26 16:18:27 +04:00
toView . CRNewChatItem $ AChatItem SCTDirect SMDRcv ( DirectChat ct ) ci
2022-01-12 06:55:04 +00:00
showToast ( c <> " > " ) " wants to send a file "
2021-09-04 07:32:56 +01:00
setActive $ ActiveC c
2022-04-10 13:30:58 +04:00
-- TODO remove once XFile is discontinued
processGroupFileInvitation' :: GroupInfo -> GroupMember -> FileInvitation -> RcvMessage -> MsgMeta -> m ()
processGroupFileInvitation' gInfo m @ GroupMember { localDisplayName = c } fInv @ FileInvitation { fileName , fileSize } msg msgMeta = do
2021-09-05 14:08:29 +01:00
chSize <- asks $ fileChunkSize . config
2022-04-10 13:30:58 +04:00
RcvFileTransfer { fileId } <- withStore $ \ st -> createRcvGroupFileTransfer st userId m fInv chSize
let ciFile = Just $ CIFile { fileId , fileName , fileSize , filePath = Nothing , fileStatus = CIFSRcvInvitation }
2022-04-30 19:18:46 +04:00
ci <- saveRcvChatItem user ( CDGroupRcv gInfo m ) msg msgMeta ( CIRcvMsgContent $ MCFile " " ) ciFile
2022-02-22 14:05:45 +00:00
groupMsgToView gInfo ci msgMeta
2022-01-27 22:01:15 +00:00
let g = groupName' gInfo
2022-01-26 16:18:27 +04:00
showToast ( " # " <> g <> " " <> c <> " > " ) " wants to send a file "
setActive $ ActiveG g
2021-09-05 14:08:29 +01:00
2022-05-11 16:18:28 +04:00
xFileCancel :: Contact -> SharedMsgId -> MsgMeta -> m ()
xFileCancel Contact { contactId } sharedMsgId msgMeta = do
2022-04-05 10:01:08 +04:00
checkIntegrity msgMeta $ toView . CRMsgIntegrityError
fileId <- withStore $ \ st -> getFileIdBySharedMsgId st userId contactId sharedMsgId
2022-05-11 16:18:28 +04:00
ft @ RcvFileTransfer { cancelled } <- withStore ( \ st -> getRcvFileTransfer st user fileId )
unless cancelled $ do
cancelRcvFileTransfer user ft
toView $ CRRcvFileSndCancelled ft
xFileCancelGroup :: GroupInfo -> GroupMember -> SharedMsgId -> MsgMeta -> m ()
xFileCancelGroup GroupInfo { groupId } GroupMember { memberId } sharedMsgId msgMeta = do
checkIntegrity msgMeta $ toView . CRMsgIntegrityError
fileId <- withStore $ \ st -> getGroupFileIdBySharedMsgId st userId groupId sharedMsgId
CChatItem msgDir ChatItem { chatDir } <- withStore $ \ st -> getGroupChatItemBySharedMsgId st user groupId sharedMsgId
case ( msgDir , chatDir ) of
( SMDRcv , CIGroupRcv m ) -> do
if sameMemberId memberId m
then do
ft @ RcvFileTransfer { cancelled } <- withStore ( \ st -> getRcvFileTransfer st user fileId )
unless cancelled $ do
cancelRcvFileTransfer user ft
toView $ CRRcvFileSndCancelled ft
else messageError " x.file.cancel: group member attempted to cancel file of another member "
( SMDSnd , _ ) -> messageError " x.file.cancel: group member attempted invalid file cancel "
2022-04-05 10:01:08 +04:00
xFileAcptInvGroup :: GroupInfo -> GroupMember -> SharedMsgId -> ConnReqInvitation -> String -> MsgMeta -> m ()
xFileAcptInvGroup GroupInfo { groupId } m sharedMsgId fileConnReq fName msgMeta = do
checkIntegrity msgMeta $ toView . CRMsgIntegrityError
fileId <- withStore $ \ st -> getGroupFileIdBySharedMsgId st userId groupId sharedMsgId
2022-05-11 16:18:28 +04:00
( FileTransferMeta { fileName , cancelled } , _ ) <- withStore ( \ st -> getSndFileTransfer st user fileId )
unless cancelled $
if fName == fileName
then
tryError ( withAgent $ \ a -> joinConnection a fileConnReq . directMessage $ XOk ) >>= \ case
Right acId ->
withStore $ \ st -> createSndGroupFileTransferConnection st userId fileId acId m
Left e -> throwError e
else messageError " x.file.acpt.inv: fileName is different from expected "
2022-04-05 10:01:08 +04:00
2022-02-22 14:05:45 +00:00
groupMsgToView :: GroupInfo -> ChatItem 'CTGroup 'MDRcv -> MsgMeta -> m ()
groupMsgToView gInfo ci msgMeta = do
checkIntegrity msgMeta $ toView . CRMsgIntegrityError
2022-05-03 10:22:35 +01:00
toView . CRNewChatItem $ AChatItem SCTGroup SMDRcv ( GroupChat gInfo ) ci
2022-02-22 14:05:45 +00:00
2021-07-24 10:26:28 +01:00
processGroupInvitation :: Contact -> GroupInvitation -> m ()
2022-01-12 06:55:04 +00:00
processGroupInvitation ct @ Contact { localDisplayName = c } inv @ ( GroupInvitation ( MemberIdRole fromMemId fromRole ) ( MemberIdRole memId memRole ) _ _ ) = do
2022-01-26 21:20:08 +00:00
when ( fromRole < GRAdmin || fromRole < memRole ) $ throwChatError ( CEGroupContactRole c )
when ( fromMemId == memId ) $ throwChatError CEGroupDuplicateMemberId
2022-01-26 16:18:27 +04:00
gInfo @ GroupInfo { localDisplayName = gName } <- withStore $ \ st -> createGroupInvitation st user ct inv
toView $ CRReceivedGroupInvitation gInfo ct memRole
2022-01-24 16:07:17 +00:00
showToast ( " # " <> gName <> " " <> c <> " > " ) " invited you to join the group "
2021-07-12 19:00:03 +01:00
2022-02-02 11:43:52 +00:00
checkIntegrity :: MsgMeta -> ( MsgErrorType -> m () ) -> m ()
checkIntegrity MsgMeta { integrity } action = case integrity of
MsgError e -> action e
MsgOk -> pure ()
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'
2022-01-24 16:07:17 +00:00
toView $ CRContactUpdated c c'
2021-08-22 15:56:36 +01:00
2022-01-11 08:50:44 +00:00
xInfoProbe :: Contact -> Probe -> m ()
2021-07-27 08:08:05 +01:00
xInfoProbe c2 probe = do
r <- withStore $ \ st -> matchReceivedProbe st userId c2 probe
forM_ r $ \ c1 -> probeMatch c1 c2 probe
2022-01-11 08:50:44 +00:00
xInfoProbeCheck :: Contact -> ProbeHash -> m ()
2021-07-27 08:08:05 +01:00
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
2022-01-11 08:50:44 +00:00
probeMatch :: Contact -> Contact -> Probe -> m ()
2021-07-27 08:08:05 +01:00
probeMatch c1 @ Contact { profile = p1 } c2 @ Contact { profile = p2 } probe =
when ( p1 == p2 ) $ do
2022-02-14 18:49:42 +04:00
void . sendDirectContactMessage c1 $ XInfoProbeOk probe
2021-07-27 08:08:05 +01:00
mergeContacts c1 c2
2022-01-11 08:50:44 +00:00
xInfoProbeOk :: Contact -> Probe -> m ()
2021-07-27 08:08:05 +01:00
xInfoProbeOk c1 probe = do
r <- withStore $ \ st -> matchSentProbe st userId c1 probe
forM_ r $ \ c2 -> mergeContacts c1 c2
2022-05-03 10:22:35 +01:00
-- to party accepting call
xCallInv :: Contact -> CallId -> CallInvitation -> RcvMessage -> MsgMeta -> m ()
2022-05-04 13:31:00 +01:00
xCallInv ct @ Contact { contactId } callId CallInvitation { callType , callDhPubKey } msg @ RcvMessage { msgId } msgMeta = do
2022-05-03 10:22:35 +01:00
checkIntegrity msgMeta $ toView . CRMsgIntegrityError
2022-05-18 07:01:32 +01:00
dhKeyPair <- if encryptedCall callType then Just <$> liftIO C . generateKeyPair' else pure Nothing
2022-05-04 13:31:00 +01:00
ci <- saveCallItem CISCallPending
let sharedKey = C . Key . C . dhBytes' <$> ( C . dh' <$> callDhPubKey <*> ( snd <$> dhKeyPair ) )
callState = CallInvitationReceived { peerCallType = callType , localDhPubKey = fst <$> dhKeyPair , sharedKey }
call' = Call { contactId , callId , chatItemId = chatItemId' ci , callState }
calls <- asks currentCalls
-- theoretically, the new call invitation for the current contant can mark the in-progress call as ended
-- (and replace it in ChatController)
-- practically, this should not happen
call_ <- atomically ( TM . lookupInsert contactId call' calls )
forM_ call_ $ \ call -> updateCallItemStatus userId ct call WCSDisconnected $ Just msgId
toView $ CRCallInvitation ct callType sharedKey
2022-05-03 10:22:35 +01:00
toView . CRNewChatItem $ AChatItem SCTDirect SMDRcv ( DirectChat ct ) ci
where
saveCallItem status = saveRcvChatItem user ( CDDirectRcv ct ) msg msgMeta ( CIRcvCall status 0 ) Nothing
-- to party initiating call
xCallOffer :: Contact -> CallId -> CallOffer -> RcvMessage -> MsgMeta -> m ()
xCallOffer ct callId CallOffer { callType , rtcSession , callDhPubKey } msg msgMeta = do
msgCurrentCall ct callId " x.call.offer " msg msgMeta $
\ call -> case callState call of
CallInvitationSent { localCallType , localDhPrivKey } -> do
let sharedKey = C . Key . C . dhBytes' <$> ( C . dh' <$> callDhPubKey <*> localDhPrivKey )
callState' = CallOfferReceived { localCallType , peerCallType = callType , peerCallSession = rtcSession , sharedKey }
2022-05-18 07:01:32 +01:00
askConfirmation = encryptedCall localCallType && not ( encryptedCall callType )
toView CRCallOffer { contact = ct , callType , offer = rtcSession , sharedKey , askConfirmation }
2022-05-03 10:22:35 +01:00
pure ( Just call { callState = callState' } , Just . ACIContent SMDSnd $ CISndCall CISCallAccepted 0 )
_ -> do
msgCallStateError " x.call.offer " call
pure ( Just call , Nothing )
-- to party accepting call
xCallAnswer :: Contact -> CallId -> CallAnswer -> RcvMessage -> MsgMeta -> m ()
xCallAnswer ct callId CallAnswer { rtcSession } msg msgMeta = do
msgCurrentCall ct callId " x.call.answer " msg msgMeta $
\ call -> case callState call of
CallOfferSent { localCallType , peerCallType , localCallSession , sharedKey } -> do
let callState' = CallNegotiated { localCallType , peerCallType , localCallSession , peerCallSession = rtcSession , sharedKey }
toView $ CRCallAnswer ct rtcSession
pure ( Just call { callState = callState' } , Just . ACIContent SMDRcv $ CIRcvCall CISCallNegotiated 0 )
_ -> do
msgCallStateError " x.call.answer " call
pure ( Just call , Nothing )
-- to any call party
xCallExtra :: Contact -> CallId -> CallExtraInfo -> RcvMessage -> MsgMeta -> m ()
xCallExtra ct callId CallExtraInfo { rtcExtraInfo } msg msgMeta = do
msgCurrentCall ct callId " x.call.extra " msg msgMeta $
\ call -> case callState call of
2022-05-07 06:40:46 +01:00
CallOfferReceived { localCallType , peerCallType , peerCallSession , sharedKey } -> do
-- TODO update the list of ice servers in peerCallSession
let callState' = CallOfferReceived { localCallType , peerCallType , peerCallSession , sharedKey }
toView $ CRCallExtraInfo ct rtcExtraInfo
pure ( Just call { callState = callState' } , Nothing )
2022-05-03 10:22:35 +01:00
CallNegotiated { localCallType , peerCallType , localCallSession , peerCallSession , sharedKey } -> do
2022-05-07 06:40:46 +01:00
-- TODO update the list of ice servers in peerCallSession
2022-05-03 10:22:35 +01:00
let callState' = CallNegotiated { localCallType , peerCallType , localCallSession , peerCallSession , sharedKey }
toView $ CRCallExtraInfo ct rtcExtraInfo
pure ( Just call { callState = callState' } , Nothing )
_ -> do
2022-05-04 13:31:00 +01:00
msgCallStateError " x.call.extra " call
2022-05-03 10:22:35 +01:00
pure ( Just call , Nothing )
-- to any call party
xCallEnd :: Contact -> CallId -> RcvMessage -> MsgMeta -> m ()
2022-05-07 06:40:46 +01:00
xCallEnd ct callId msg msgMeta =
2022-05-04 13:31:00 +01:00
msgCurrentCall ct callId " x.call.end " msg msgMeta $ \ Call { chatItemId } -> do
toView $ CRCallEnded ct
2022-05-07 06:40:46 +01:00
( Nothing , ) <$> callStatusItemContent userId ct chatItemId WCSDisconnected
2022-05-03 10:22:35 +01:00
msgCurrentCall :: Contact -> CallId -> Text -> RcvMessage -> MsgMeta -> ( Call -> m ( Maybe Call , Maybe ACIContent ) ) -> m ()
msgCurrentCall ct @ Contact { contactId = ctId' } callId' eventName RcvMessage { msgId } msgMeta action = do
checkIntegrity msgMeta $ toView . CRMsgIntegrityError
2022-05-04 13:31:00 +01:00
calls <- asks currentCalls
atomically ( TM . lookup ctId' calls ) >>= \ case
2022-05-03 10:22:35 +01:00
Nothing -> messageError $ eventName <> " : no current call "
Just call @ Call { contactId , callId , chatItemId }
| contactId /= ctId' || callId /= callId' -> messageError $ eventName <> " : wrong contact or callId "
| otherwise -> do
2022-05-04 13:31:00 +01:00
( call_ , aciContent_ ) <- action call
atomically $ case call_ of
Just call' -> TM . insert ctId' call' calls
_ -> TM . delete ctId' calls
forM_ aciContent_ $ \ aciContent ->
updateDirectChatItemView userId ct chatItemId aciContent $ Just msgId
2022-05-03 10:22:35 +01:00
msgCallStateError :: Text -> Call -> m ()
msgCallStateError eventName Call { callState } =
messageError $ eventName <> " : wrong call state " <> T . pack ( show $ callStateTag callState )
2021-07-27 08:08:05 +01:00
mergeContacts :: Contact -> Contact -> m ()
mergeContacts to from = do
withStore $ \ st -> mergeContactRecords st userId to from
2022-01-24 16:07:17 +00:00
toView $ CRContactsMerged to from
2021-07-27 08:08:05 +01:00
2021-07-06 19:07:03 +01:00
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
2022-02-08 13:04:17 +04:00
XInfo p -> do
ct <- withStore $ \ st -> createDirectContact st userId activeConn p
toView $ CRContactConnecting ct
2021-07-24 18:11:04 +01:00
-- TODO show/log error, other events in SMP confirmation
_ -> pure ()
2022-01-26 16:18:27 +04:00
xGrpMemNew :: GroupInfo -> GroupMember -> MemberInfo -> m ()
xGrpMemNew gInfo m memInfo @ ( MemberInfo memId _ _ ) = do
members <- withStore $ \ st -> getGroupMembers st user gInfo
unless ( sameMemberId memId $ membership gInfo ) $
if isMember memId gInfo members
2021-07-24 18:11:04 +01:00
then messageError " x.grp.mem.new error: member already exists "
else do
2022-01-26 16:18:27 +04:00
newMember <- withStore $ \ st -> createNewGroupMember st user gInfo memInfo GCPostMember GSMemAnnounced
toView $ CRJoinedGroupMemberConnecting gInfo m newMember
2021-07-24 18:11:04 +01:00
2022-01-26 16:18:27 +04:00
xGrpMemIntro :: Connection -> GroupInfo -> GroupMember -> MemberInfo -> m ()
2022-02-25 21:59:35 +04:00
xGrpMemIntro conn gInfo @ GroupInfo { groupId } m memInfo @ ( MemberInfo memId _ _ ) = do
2021-07-24 18:11:04 +01:00
case memberCategory m of
GCHostMember -> do
2022-01-26 16:18:27 +04:00
members <- withStore $ \ st -> getGroupMembers st user gInfo
if isMember memId gInfo members
2021-07-24 18:11:04 +01:00
then messageWarning " x.grp.mem.intro ignored: member already exists "
else do
2021-12-08 13:09:51 +00:00
( groupConnId , groupConnReq ) <- withAgent ( ` createConnection ` SCMInvitation )
( directConnId , directConnReq ) <- withAgent ( ` createConnection ` SCMInvitation )
2022-01-26 16:18:27 +04:00
newMember <- withStore $ \ st -> createIntroReMember st user gInfo m memInfo groupConnId directConnId
2021-12-02 11:17:09 +00:00
let msg = XGrpMemInv memId IntroInvitation { groupConnReq , directConnReq }
2022-02-25 21:59:35 +04:00
void $ sendDirectMessage conn msg ( GroupId groupId )
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 "
2022-01-26 16:18:27 +04:00
xGrpMemInv :: GroupInfo -> GroupMember -> MemberId -> IntroInvitation -> m ()
xGrpMemInv gInfo m memId introInv = do
2021-07-24 18:11:04 +01:00
case memberCategory m of
GCInviteeMember -> do
2022-01-26 16:18:27 +04:00
members <- withStore $ \ st -> getGroupMembers st user gInfo
case find ( sameMemberId memId ) members of
2022-03-28 20:35:57 +04:00
Nothing -> messageError " x.grp.mem.inv error: referenced member does not exist "
2021-07-24 18:11:04 +01:00
Just reMember -> do
2022-01-24 16:07:17 +00:00
GroupMemberIntro { introId } <- withStore $ \ st -> saveIntroInvitation st reMember m introInv
2022-02-25 21:59:35 +04:00
void $ sendXGrpMemInv gInfo reMember ( XGrpMemFwd ( memberInfo m ) introInv ) introId
2021-07-24 18:11:04 +01:00
_ -> messageError " x.grp.mem.inv can be only sent by invitee member "
2022-01-26 16:18:27 +04:00
xGrpMemFwd :: GroupInfo -> GroupMember -> MemberInfo -> IntroInvitation -> m ()
xGrpMemFwd gInfo @ GroupInfo { membership } m memInfo @ ( MemberInfo memId _ _ ) introInv @ IntroInvitation { groupConnReq , directConnReq } = do
members <- withStore $ \ st -> getGroupMembers st user gInfo
toMember <- case find ( sameMemberId memId ) members of
2021-07-24 18:11:04 +01:00
-- 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.
2022-01-26 16:18:27 +04:00
Nothing -> withStore $ \ st -> createNewGroupMember st user gInfo memInfo GCPostMember GSMemAnnounced
2021-07-24 18:11:04 +01:00
Just m' -> pure m'
withStore $ \ st -> saveMemberInvitation st toMember introInv
2022-01-11 08:50:44 +00:00
let msg = XGrpMemInfo ( memberId ( membership :: GroupMember ) ) 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
2022-01-26 16:18:27 +04:00
xGrpMemDel :: GroupInfo -> GroupMember -> MemberId -> m ()
xGrpMemDel gInfo @ GroupInfo { membership } m memId = do
members <- withStore $ \ st -> getGroupMembers st user gInfo
2022-01-11 08:50:44 +00:00
if memberId ( membership :: GroupMember ) == memId
2021-08-02 20:10:24 +01:00
then do
mapM_ deleteMemberConnection members
withStore $ \ st -> updateGroupMemberStatus st userId membership GSMemRemoved
2022-01-26 16:18:27 +04:00
toView $ CRDeletedMemberUser gInfo m
2022-01-11 08:50:44 +00:00
else case find ( sameMemberId memId ) members of
2021-08-02 20:10:24 +01:00
Nothing -> messageError " x.grp.mem.del with unknown member ID "
Just member -> do
2022-01-11 08:50:44 +00:00
let mRole = memberRole ( m :: GroupMember )
if mRole < GRAdmin || mRole < memberRole ( member :: GroupMember )
2021-08-02 20:10:24 +01:00
then messageError " x.grp.mem.del with insufficient member permissions "
else do
deleteMemberConnection member
withStore $ \ st -> updateGroupMemberStatus st userId member GSMemRemoved
2022-01-26 16:18:27 +04:00
toView $ CRDeletedMember gInfo m member
2021-08-02 20:10:24 +01:00
2022-01-11 08:50:44 +00:00
sameMemberId :: MemberId -> GroupMember -> Bool
sameMemberId memId GroupMember { memberId } = memId == memberId
2022-01-26 16:18:27 +04:00
xGrpLeave :: GroupInfo -> GroupMember -> m ()
xGrpLeave gInfo m = do
2021-08-02 20:10:24 +01:00
deleteMemberConnection m
withStore $ \ st -> updateGroupMemberStatus st userId m GSMemLeft
2022-01-26 16:18:27 +04:00
toView $ CRLeftMember gInfo m
2021-08-02 20:10:24 +01:00
2022-01-26 16:18:27 +04:00
xGrpDel :: GroupInfo -> GroupMember -> m ()
xGrpDel gInfo m @ GroupMember { memberRole } = do
2022-01-26 21:20:08 +00:00
when ( memberRole /= GROwner ) $ throwChatError CEGroupUserRole
2021-08-02 20:10:24 +01:00
ms <- withStore $ \ st -> do
2022-01-26 16:18:27 +04:00
members <- getGroupMembers st user gInfo
updateGroupMemberStatus st userId ( membership gInfo ) GSMemGroupDeleted
2021-08-02 20:10:24 +01:00
pure members
mapM_ deleteMemberConnection ms
2022-01-26 16:18:27 +04:00
toView $ CRGroupDeleted gInfo m
2021-08-02 20:10:24 +01:00
2021-12-29 23:11:55 +04:00
parseChatMessage :: ByteString -> Either ChatError ChatMessage
2022-02-06 16:18:01 +00:00
parseChatMessage = first ( ChatError . CEInvalidChatMessage ) . strDecode
2021-12-29 23:11:55 +04:00
2022-05-05 10:37:53 +01:00
sendFileChunk :: ChatMonad m => User -> SndFileTransfer -> m ()
sendFileChunk user ft @ SndFileTransfer { fileId , fileStatus , agentConnId = AgentConnId acId } =
2021-09-04 07:32:56 +01:00
unless ( fileStatus == FSComplete || fileStatus == FSCancelled ) $
withStore ( ` createSndFileChunk ` ft ) >>= \ case
Just chunkNo -> sendFileChunkNo ft chunkNo
Nothing -> do
2022-05-05 10:37:53 +01:00
ci <- withStore $ \ st -> do
2021-09-05 14:08:29 +01:00
updateSndFileStatus st ft FSComplete
deleteSndFileChunks st ft
2022-05-05 13:50:19 +01:00
updateDirectCIFileStatus st user fileId CIFSSndComplete
2022-05-05 10:37:53 +01:00
toView $ CRSndFileComplete ci ft
2021-09-04 07:32:56 +01:00
closeFileHandle fileId sndFiles
2022-01-26 16:18:27 +04:00
withAgent ( ` deleteConnection ` acId )
2021-09-04 07:32:56 +01:00
sendFileChunkNo :: ChatMonad m => SndFileTransfer -> Integer -> m ()
2022-01-26 16:18:27 +04:00
sendFileChunkNo ft @ SndFileTransfer { agentConnId = AgentConnId acId } chunkNo = do
2022-01-11 12:41:38 +00:00
chunkBytes <- readFileChunk ft chunkNo
2022-01-26 16:18:27 +04:00
msgId <- withAgent $ \ a -> sendMessage a acId $ smpEncode FileChunk { chunkNo , chunkBytes }
2021-09-04 07:32:56 +01:00
withStore $ \ st -> updateSndFileChunkMsg st ft chunkNo msgId
readFileChunk :: ChatMonad m => SndFileTransfer -> Integer -> m ByteString
2022-04-15 09:36:38 +04:00
readFileChunk SndFileTransfer { fileId , filePath , chunkSize } chunkNo = do
fsFilePath <- toFSFilePath filePath
read_ fsFilePath ` E . catch ` ( throwChatError . CEFileRead filePath . ( show :: E . SomeException -> String ) )
2021-09-04 07:32:56 +01:00
where
2022-04-15 09:36:38 +04:00
read_ fsFilePath = do
h <- getFileHandle fileId fsFilePath sndFiles ReadMode
2021-09-04 07:32:56 +01:00
pos <- hTell h
let pos' = ( chunkNo - 1 ) * chunkSize
when ( pos /= pos' ) $ hSeek h AbsoluteSeek pos'
liftIO . B . hGet h $ fromInteger chunkSize
2022-01-11 12:41:38 +00:00
data FileChunk = FileChunk { chunkNo :: Integer , chunkBytes :: ByteString } | FileChunkCancel
instance Encoding FileChunk where
smpEncode = \ case
FileChunk { chunkNo , chunkBytes } -> smpEncode ( 'F' , fromIntegral chunkNo :: Word32 , Tail chunkBytes )
FileChunkCancel -> smpEncode 'C'
smpP =
smpP >>= \ case
'F' -> do
chunkNo <- fromIntegral <$> smpP @ Word32
Tail chunkBytes <- smpP
pure FileChunk { chunkNo , chunkBytes }
'C' -> pure FileChunkCancel
_ -> fail " bad FileChunk "
parseFileChunk :: ChatMonad m => ByteString -> m FileChunk
2021-09-04 07:32:56 +01:00
parseFileChunk msg =
2022-01-11 12:41:38 +00:00
liftEither . first ( ChatError . CEFileRcvChunk ) $ parseAll smpP msg
2021-09-04 07:32:56 +01:00
appendFileChunk :: ChatMonad m => RcvFileTransfer -> Integer -> ByteString -> m ()
appendFileChunk ft @ RcvFileTransfer { fileId , fileStatus } chunkNo chunk =
case fileStatus of
2022-05-12 17:37:09 +04:00
RFSConnected RcvFileInfo { filePath } -> append_ filePath
-- sometimes update of file transfer status to FSConnected
-- doesn't complete in time before MSG with first file chunk
RFSAccepted RcvFileInfo { filePath } -> append_ filePath
2021-09-04 07:32:56 +01:00
RFSCancelled _ -> pure ()
2022-01-26 21:20:08 +00:00
_ -> throwChatError $ CEFileInternal " receiving file transfer not in progress "
2021-09-04 07:32:56 +01:00
where
2022-05-12 17:37:09 +04:00
append_ filePath = do
fsFilePath <- toFSFilePath filePath
h <- getFileHandle fileId fsFilePath rcvFiles AppendMode
2021-09-04 07:32:56 +01:00
E . try ( liftIO $ B . hPut h chunk >> hFlush h ) >>= \ case
2022-05-12 17:37:09 +04:00
Left ( e :: E . SomeException ) -> throwChatError . CEFileWrite fsFilePath $ show e
2021-09-04 07:32:56 +01:00
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
2022-05-11 16:18:28 +04:00
cancelRcvFileTransfer :: ChatMonad m => User -> RcvFileTransfer -> m ()
cancelRcvFileTransfer user ft @ RcvFileTransfer { fileId , fileStatus } = do
2021-09-04 07:32:56 +01:00
closeFileHandle fileId rcvFiles
2021-09-05 14:08:29 +01:00
withStore $ \ st -> do
2022-05-11 16:18:28 +04:00
updateFileCancelled st user fileId CIFSRcvCancelled
2021-09-05 14:08:29 +01:00
updateRcvFileStatus st ft FSCancelled
deleteRcvFileChunks st ft
2021-09-04 07:32:56 +01:00
case fileStatus of
2022-05-09 10:55:56 +04:00
RFSAccepted RcvFileInfo { agentConnId = AgentConnId acId } ->
2022-05-07 18:24:38 +04:00
withAgent ( ` deleteConnection ` acId )
2022-05-09 10:55:56 +04:00
RFSConnected RcvFileInfo { agentConnId = AgentConnId acId } ->
2022-05-07 18:24:38 +04:00
withAgent ( ` deleteConnection ` acId )
2021-09-04 07:32:56 +01:00
_ -> pure ()
2022-05-11 16:18:28 +04:00
cancelSndFile :: ChatMonad m => User -> FileTransferMeta -> [ SndFileTransfer ] -> m ()
cancelSndFile user FileTransferMeta { fileId } fts = do
withStore $ \ st -> updateFileCancelled st user fileId CIFSSndCancelled
forM_ fts $ \ ft' -> cancelSndFileTransfer ft'
2021-09-04 07:32:56 +01:00
cancelSndFileTransfer :: ChatMonad m => SndFileTransfer -> m ()
2022-05-09 10:55:56 +04:00
cancelSndFileTransfer ft @ SndFileTransfer { agentConnId = AgentConnId acId , fileStatus } =
2021-09-04 07:32:56 +01:00
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
2022-01-26 16:18:27 +04:00
void ( sendMessage a acId $ smpEncode FileChunkCancel ) ` catchError ` \ _ -> pure ()
2022-04-26 12:52:41 +04:00
deleteConnection a acId
2021-09-04 07:32:56 +01:00
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 ()
2022-01-26 21:20:08 +00:00
throwChatError :: ChatMonad m => ChatErrorType -> m a
throwChatError = throwError . ChatError
2021-08-02 20:10:24 +01:00
deleteMemberConnection :: ChatMonad m => GroupMember -> m ()
2021-08-14 21:04:51 +01:00
deleteMemberConnection m @ GroupMember { activeConn } = do
-- User {userId} <- asks currentUser
2022-02-12 13:17:11 +04:00
withAgent ( forM_ ( memberConnId m ) . deleteConnection ) ` catchError ` const ( pure () )
2021-08-14 21:04:51 +01:00
-- withStore $ \st -> deleteGroupMemberConnection st userId m
forM_ activeConn $ \ conn -> withStore $ \ st -> updateConnectionStatus st conn ConnDeleted
2021-08-02 20:10:24 +01:00
2022-03-13 19:34:03 +00:00
sendDirectContactMessage :: ChatMonad m => Contact -> ChatMsgEvent -> m SndMessage
2022-02-25 21:59:35 +04:00
sendDirectContactMessage ct @ Contact { activeConn = conn @ Connection { connId , connStatus } } chatMsgEvent = do
2022-02-14 18:49:42 +04:00
if connStatus == ConnReady || connStatus == ConnSndReady
2022-02-25 21:59:35 +04:00
then sendDirectMessage conn chatMsgEvent ( ConnectionId connId )
2022-02-14 18:49:42 +04:00
else throwChatError $ CEContactNotReady ct
2022-03-13 19:34:03 +00:00
sendDirectMessage :: ChatMonad m => Connection -> ChatMsgEvent -> ConnOrGroupId -> m SndMessage
2022-02-25 21:59:35 +04:00
sendDirectMessage conn chatMsgEvent connOrGroupId = do
2022-03-13 19:34:03 +00:00
msg @ SndMessage { msgId , msgBody } <- createSndMessage chatMsgEvent connOrGroupId
2021-12-29 23:11:55 +04:00
deliverMessage conn msgBody msgId
2022-03-13 19:34:03 +00:00
pure msg
2022-01-24 16:07:17 +00:00
2022-03-13 19:34:03 +00:00
createSndMessage :: ChatMonad m => ChatMsgEvent -> ConnOrGroupId -> m SndMessage
2022-02-25 21:59:35 +04:00
createSndMessage chatMsgEvent connOrGroupId = do
2022-03-13 19:34:03 +00:00
gVar <- asks idsDrg
withStore $ \ st -> createNewSndMessage st gVar connOrGroupId $ \ sharedMsgId ->
let msgBody = strEncode ChatMessage { msgId = Just sharedMsgId , chatMsgEvent }
2022-03-16 13:20:47 +00:00
in NewMessage { chatMsgEvent , msgBody }
2021-07-16 07:40:55 +01:00
directMessage :: ChatMsgEvent -> ByteString
2022-03-13 19:34:03 +00:00
directMessage chatMsgEvent = strEncode ChatMessage { msgId = Nothing , chatMsgEvent }
2021-07-16 07:40:55 +01:00
2021-12-29 23:11:55 +04:00
deliverMessage :: ChatMonad m => Connection -> MsgBody -> MessageId -> m ()
2022-01-26 21:20:08 +00:00
deliverMessage conn @ Connection { connId } msgBody msgId = do
agentMsgId <- withAgent $ \ a -> sendMessage a ( aConnId conn ) msgBody
2021-12-29 23:11:55 +04:00
let sndMsgDelivery = SndMsgDelivery { connId , agentMsgId }
withStore $ \ st -> createSndMsgDelivery st sndMsgDelivery msgId
2022-03-13 19:34:03 +00:00
sendGroupMessage :: ChatMonad m => GroupInfo -> [ GroupMember ] -> ChatMsgEvent -> m SndMessage
2022-02-25 21:59:35 +04:00
sendGroupMessage GroupInfo { groupId } members chatMsgEvent =
sendGroupMessage' members chatMsgEvent groupId Nothing $ pure ()
2022-01-24 16:07:17 +00:00
2022-03-13 19:34:03 +00:00
sendXGrpMemInv :: ChatMonad m => GroupInfo -> GroupMember -> ChatMsgEvent -> Int64 -> m SndMessage
2022-02-25 21:59:35 +04:00
sendXGrpMemInv GroupInfo { groupId } reMember chatMsgEvent introId =
sendGroupMessage' [ reMember ] chatMsgEvent groupId ( Just introId ) $
2022-01-24 16:07:17 +00:00
withStore ( \ st -> updateIntroStatus st introId GMIntroInvForwarded )
2022-03-13 19:34:03 +00:00
sendGroupMessage' :: ChatMonad m => [ GroupMember ] -> ChatMsgEvent -> Int64 -> Maybe Int64 -> m () -> m SndMessage
2022-02-25 21:59:35 +04:00
sendGroupMessage' members chatMsgEvent groupId introId_ postDeliver = do
2022-03-13 19:34:03 +00:00
msg @ SndMessage { msgId , msgBody } <- createSndMessage chatMsgEvent ( GroupId groupId )
2022-02-10 17:03:36 +04:00
-- TODO collect failed deliveries into a single error
forM_ ( filter memberCurrent members ) $ \ m @ GroupMember { groupMemberId } ->
2022-01-24 16:07:17 +00:00
case memberConn m of
Nothing -> withStore $ \ st -> createPendingGroupMessage st groupMemberId msgId introId_
2022-02-10 17:03:36 +04:00
Just conn @ Connection { connStatus } ->
if not ( connStatus == ConnSndReady || connStatus == ConnReady )
then unless ( connStatus == ConnDeleted ) $ withStore ( \ st -> createPendingGroupMessage st groupMemberId msgId introId_ )
else ( deliverMessage conn msgBody msgId >> postDeliver ) ` catchError ` const ( pure () )
2022-03-13 19:34:03 +00:00
pure msg
2022-01-24 16:07:17 +00:00
sendPendingGroupMessages :: ChatMonad m => GroupMember -> Connection -> m ()
sendPendingGroupMessages GroupMember { groupMemberId , localDisplayName } conn = do
pendingMessages <- withStore $ \ st -> getPendingGroupMessages st groupMemberId
-- TODO ensure order - pending messages interleave with user input messages
2022-02-10 17:03:36 +04:00
forM_ pendingMessages $ \ PendingGroupMessage { msgId , cmEventTag , msgBody , introId_ } -> do
2022-01-24 16:07:17 +00:00
deliverMessage conn msgBody msgId
withStore ( \ st -> deletePendingGroupMessage st groupMemberId msgId )
when ( cmEventTag == XGrpMemFwd_ ) $ case introId_ of
2022-01-26 21:20:08 +00:00
Nothing -> throwChatError $ CEGroupMemberIntroNotFound localDisplayName
2022-01-24 16:07:17 +00:00
Just introId -> withStore ( \ st -> updateIntroStatus st introId GMIntroInvForwarded )
2022-03-16 13:20:47 +00:00
saveRcvMSG :: ChatMonad m => Connection -> ConnOrGroupId -> MsgMeta -> MsgBody -> m RcvMessage
2022-03-13 19:34:03 +00:00
saveRcvMSG Connection { connId } connOrGroupId agentMsgMeta msgBody = do
ChatMessage { msgId = sharedMsgId_ , chatMsgEvent } <- liftEither $ parseChatMessage msgBody
2022-01-24 16:07:17 +00:00
let agentMsgId = fst $ recipient agentMsgMeta
2022-03-16 13:20:47 +00:00
newMsg = NewMessage { chatMsgEvent , msgBody }
2021-12-29 23:11:55 +04:00
rcvMsgDelivery = RcvMsgDelivery { connId , agentMsgId , agentMsgMeta }
2022-03-13 19:34:03 +00:00
withStore $ \ st -> createNewMessageAndRcvMsgDelivery st connOrGroupId newMsg sharedMsgId_ rcvMsgDelivery
2022-01-26 16:18:27 +04:00
2022-04-10 13:30:58 +04:00
saveSndChatItem :: ChatMonad m => User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> Maybe ( CIFile 'MDSnd ) -> Maybe ( CIQuote c ) -> m ( ChatItem c 'MDSnd )
saveSndChatItem user cd msg @ SndMessage { sharedMsgId } content ciFile quotedItem = do
2022-01-28 11:52:10 +04:00
createdAt <- liftIO getCurrentTime
2022-03-16 13:20:47 +00:00
ciId <- withStore $ \ st -> createNewSndChatItem st user cd msg content quotedItem createdAt
2022-04-10 13:30:58 +04:00
forM_ ciFile $ \ CIFile { fileId } -> withStore $ \ st -> updateFileTransferChatItemId st fileId ciId
liftIO $ mkChatItem cd ciId content ciFile quotedItem ( Just sharedMsgId ) createdAt createdAt
2022-01-26 16:18:27 +04:00
2022-04-10 13:30:58 +04:00
saveRcvChatItem :: ChatMonad m => User -> ChatDirection c 'MDRcv -> RcvMessage -> MsgMeta -> CIContent 'MDRcv -> Maybe ( CIFile 'MDRcv ) -> m ( ChatItem c 'MDRcv )
2022-05-17 11:22:09 +04:00
saveRcvChatItem user cd msg @ RcvMessage { sharedMsgId_ } = saveRcvChatItem' user cd msg sharedMsgId_
saveRcvChatItem' :: ChatMonad m => User -> ChatDirection c 'MDRcv -> RcvMessage -> Maybe SharedMsgId -> MsgMeta -> CIContent 'MDRcv -> Maybe ( CIFile 'MDRcv ) -> m ( ChatItem c 'MDRcv )
saveRcvChatItem' user cd msg sharedMsgId_ MsgMeta { broker = ( _ , brokerTs ) } content ciFile = do
2022-01-28 11:52:10 +04:00
createdAt <- liftIO getCurrentTime
2022-05-17 11:22:09 +04:00
( ciId , quotedItem ) <- withStore $ \ st -> createNewRcvChatItem st user cd msg sharedMsgId_ content brokerTs createdAt
2022-04-10 13:30:58 +04:00
forM_ ciFile $ \ CIFile { fileId } -> withStore $ \ st -> updateFileTransferChatItemId st fileId ciId
liftIO $ mkChatItem cd ciId content ciFile quotedItem sharedMsgId_ brokerTs createdAt
2022-03-16 13:20:47 +00:00
2022-04-10 13:30:58 +04:00
mkChatItem :: MsgDirectionI d => ChatDirection c d -> ChatItemId -> CIContent d -> Maybe ( CIFile d ) -> Maybe ( CIQuote c ) -> Maybe SharedMsgId -> ChatItemTs -> UTCTime -> IO ( ChatItem c d )
2022-05-04 13:31:00 +01:00
mkChatItem cd ciId content file quotedItem sharedMsgId itemTs currentTs = do
2022-03-16 13:20:47 +00:00
tz <- getCurrentTimeZone
let itemText = ciContentToText content
2022-05-04 13:31:00 +01:00
meta = mkCIMeta ciId content itemText ciStatusNew sharedMsgId False False tz currentTs itemTs currentTs currentTs
2022-04-10 13:30:58 +04:00
pure ChatItem { chatDir = toCIDirection cd , meta , content , formattedText = parseMaybeMarkdownList itemText , quotedItem , file }
2021-07-24 10:26:28 +01:00
2021-12-08 13:09:51 +00:00
allowAgentConnection :: ChatMonad m => Connection -> ConfirmationId -> ChatMsgEvent -> m ()
2022-01-26 21:20:08 +00:00
allowAgentConnection conn confId msg = do
withAgent $ \ a -> allowConnection a ( aConnId conn ) 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) "
2022-03-10 15:45:40 +04:00
liftIO ( runExceptT $ createUser st Profile { displayName , fullName , image = Nothing } True ) >>= \ case
2021-07-14 20:11:41 +01:00
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
2022-03-13 20:13:47 +00:00
showMsgToast :: ( MonadUnliftIO m , MonadReader ChatController m ) => Text -> MsgContent -> Maybe MarkdownList -> m ()
showMsgToast from mc md_ = showToast from $ maybe ( msgContentText mc ) ( mconcat . map hideSecret ) md_
where
hideSecret :: FormattedText -> Text
hideSecret FormattedText { format = Just Secret } = " ... "
hideSecret FormattedText { text } = text
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
2022-02-06 16:18:01 +00:00
withUser' :: ChatMonad m => ( User -> m a ) -> m a
withUser' action =
asks currentUser
>>= readTVarIO
>>= maybe ( throwChatError CENoActiveUser ) action
withUser :: ChatMonad m => ( User -> m a ) -> m a
withUser action = withUser' $ \ user ->
ifM chatStarted ( action user ) ( throwChatError CEChatNotStarted )
where
chatStarted = fmap isJust . readTVarIO =<< asks agentAsync
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
2022-05-09 10:55:56 +04:00
>>= runExceptT . action
2022-03-10 15:45:40 +04:00
-- use this line instead of above to log query errors
2022-05-09 10:55:56 +04:00
-- >>= (\st -> runExceptT $ action st `E.catch` \(e :: E.SomeException) -> liftIO (print e) >> E.throwIO e)
2021-07-12 19:00:03 +01:00
>>= liftEither . first ChatErrorStore
2021-07-04 18:42:24 +01:00
2021-06-25 18:18:24 +01:00
chatCommandP :: Parser ChatCommand
chatCommandP =
2022-02-06 16:18:01 +00:00
( " /user " <|> " /u " ) *> ( CreateActiveUser <$> userProfile )
<|> ( " /user " <|> " /u " ) $> ShowActiveUser
<|> " /_start " $> StartChat
2022-04-30 12:47:50 +01:00
<|> " /_resubscribe all " $> ResubscribeAllConnections
2022-04-15 09:36:38 +04:00
<|> " /_files_folder " *> ( SetFilesFolder <$> filePath )
2022-04-24 09:05:54 +01:00
<|> " /_get chats " *> ( APIGetChats <$> ( " pcc=on " $> True <|> " pcc=off " $> False <|> pure False ) )
2022-04-28 08:34:21 +01:00
<|> " /_get chat " *> ( APIGetChat <$> chatRefP <* A . space <*> chatPaginationP )
2022-01-31 23:20:52 +00:00
<|> " /_get items count= " *> ( APIGetChatItems <$> A . decimal )
2022-05-06 09:17:49 +01:00
<|> " /_send " *> ( APISendMessage <$> chatRefP <*> ( " json " *> jsonP <|> " text " *> ( ComposedMessage Nothing Nothing <$> mcTextP ) ) )
2022-04-28 08:34:21 +01:00
<|> " /_update item " *> ( APIUpdateChatItem <$> chatRefP <* A . space <*> A . decimal <* A . space <*> msgContentP )
<|> " /_delete item " *> ( APIDeleteChatItem <$> chatRefP <* A . space <*> A . decimal <* A . space <*> ciDeleteMode )
2022-05-13 09:38:14 +01:00
<|> " /_read chat " *> ( APIChatRead <$> chatRefP <*> optional ( A . space *> ( ( , ) <$> ( " from= " *> A . decimal ) <* A . space <*> ( " to= " *> A . decimal ) ) ) )
2022-04-28 08:34:21 +01:00
<|> " /_delete " *> ( APIDeleteChat <$> chatRefP )
2022-05-17 11:22:09 +04:00
<|> " /_clear chat " *> ( APIClearChat <$> chatRefP )
2022-01-31 23:20:52 +00:00
<|> " /_accept " *> ( APIAcceptContact <$> A . decimal )
2022-02-01 05:31:34 +00:00
<|> " /_reject " *> ( APIRejectContact <$> A . decimal )
2022-05-02 17:06:49 +01:00
<|> " /_call invite @ " *> ( APISendCallInvitation <$> A . decimal <* A . space <*> jsonP )
2022-05-17 08:37:00 +01:00
<|> ( " /call @ " <|> " /call " ) *> ( SendCallInvitation <$> displayName <*> pure defaultCallType )
2022-05-02 17:06:49 +01:00
<|> " /_call reject @ " *> ( APIRejectCall <$> A . decimal )
<|> " /_call offer @ " *> ( APISendCallOffer <$> A . decimal <* A . space <*> jsonP )
<|> " /_call answer @ " *> ( APISendCallAnswer <$> A . decimal <* A . space <*> jsonP )
<|> " /_call extra @ " *> ( APISendCallExtraInfo <$> A . decimal <* A . space <*> jsonP )
<|> " /_call end @ " *> ( APIEndCall <$> A . decimal )
2022-05-04 13:31:00 +01:00
<|> " /_call status @ " *> ( APICallStatus <$> A . decimal <* A . space <*> strP )
2022-03-23 20:52:00 +00:00
<|> " /_profile " *> ( APIUpdateProfile <$> jsonP )
2022-04-04 19:51:49 +01:00
<|> " /_parse " *> ( APIParseMarkdown . safeDecodeUtf8 <$> A . takeByteString )
2022-04-21 20:04:22 +01:00
<|> " /_ntf register " *> ( APIRegisterToken <$> tokenP )
<|> " /_ntf verify " *> ( APIVerifyToken <$> tokenP <* A . space <*> strP <* A . space <*> strP )
<|> " /_ntf interval " *> ( APIIntervalNofication <$> tokenP <* A . space <*> A . decimal )
<|> " /_ntf delete " *> ( APIDeleteToken <$> tokenP )
2022-03-10 15:45:40 +04:00
<|> " /smp_servers default " $> SetUserSMPServers []
<|> " /smp_servers " *> ( SetUserSMPServers <$> smpServersP )
<|> " /smp_servers " $> GetUserSMPServers
2022-01-28 10:41:09 +00:00
<|> ( " /help files " <|> " /help file " <|> " /hf " ) $> ChatHelp HSFiles
2022-01-24 16:07:17 +00:00
<|> ( " /help groups " <|> " /help group " <|> " /hg " ) $> ChatHelp HSGroups
<|> ( " /help address " <|> " /ha " ) $> ChatHelp HSMyAddress
2022-04-03 09:44:23 +01:00
<|> ( " /help messages " <|> " /hm " ) $> ChatHelp HSMessages
2022-01-24 16:07:17 +00:00
<|> ( " /help " <|> " /h " ) $> ChatHelp HSMain
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 )
2022-05-17 11:22:09 +04:00
<|> ( " /delete @ " <|> " /delete " <|> " /d @ " <|> " /d " ) *> ( DeleteContact <$> displayName )
<|> " /clear # " *> ( ClearGroup <$> displayName )
<|> ( " /clear @ " <|> " /clear " ) *> ( ClearContact <$> displayName )
2021-07-27 08:08:05 +01:00
<|> ( " /members # " <|> " /members " <|> " /ms # " <|> " /ms " ) *> ( ListMembers <$> displayName )
2021-12-10 11:45:58 +00:00
<|> ( " /groups " <|> " /gs " ) $> ListGroups
2022-03-19 09:04:53 +00:00
<|> ( " ># " <|> " > # " ) *> ( SendGroupMessageQuote <$> displayName <* A . space <*> pure Nothing <*> quotedMsg <*> A . takeByteString )
<|> ( " ># " <|> " > # " ) *> ( SendGroupMessageQuote <$> displayName <* A . space <* optional ( A . char '@' ) <*> ( Just <$> displayName ) <* A . space <*> quotedMsg <*> A . takeByteString )
2021-12-10 11:45:58 +00:00
<|> ( " /contacts " <|> " /cs " ) $> ListContacts
2022-01-11 08:50:44 +00:00
<|> ( " /connect " <|> " /c " ) *> ( Connect <$> ( ( Just <$> strP ) <|> A . takeByteString $> Nothing ) )
2021-08-02 20:10:24 +01:00
<|> ( " /connect " <|> " /c " ) $> AddContact
2022-04-28 08:34:21 +01:00
<|> ( SendMessage <$> chatNameP <* A . space <*> A . takeByteString )
2022-03-13 19:34:03 +00:00
<|> ( " >@ " <|> " > @ " ) *> sendMsgQuote ( AMsgDirection SMDRcv )
<|> ( " >>@ " <|> " >> @ " ) *> sendMsgQuote ( AMsgDirection SMDSnd )
2022-04-28 08:34:21 +01:00
<|> ( " \ \ " <|> " \ \ " ) *> ( DeleteMessage <$> chatNameP <* A . space <*> A . takeByteString )
<|> ( " ! " <|> " ! " ) *> ( EditMessage <$> chatNameP <* A . space <*> ( quotedMsg <|> pure " " ) <*> A . takeByteString )
2022-03-29 08:53:30 +01:00
<|> " /feed " *> ( SendMessageBroadcast <$> A . takeByteString )
2022-04-28 08:34:21 +01:00
<|> ( " /tail " <|> " /t " ) *> ( LastMessages <$> optional ( A . space *> chatNameP ) <*> msgCountP )
2022-04-30 19:18:46 +04:00
<|> ( " /file " <|> " /f " ) *> ( SendFile <$> chatNameP' <* 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 )
2022-03-29 08:53:30 +01:00
<|> " /simplex " $> ConnectSimplex
2021-12-08 13:09:51 +00:00
<|> ( " /address " <|> " /ad " ) $> CreateMyAddress
<|> ( " /delete_address " <|> " /da " ) $> DeleteMyAddress
<|> ( " /show_address " <|> " /sa " ) $> ShowMyAddress
2022-02-14 14:59:11 +04:00
<|> " /auto_accept " *> ( AddressAutoAccept <$> onOffP )
2021-12-08 13:09:51 +00:00
<|> ( " /accept @ " <|> " /accept " <|> " /ac @ " <|> " /ac " ) *> ( AcceptContact <$> displayName )
<|> ( " /reject @ " <|> " /reject " <|> " /rc @ " <|> " /rc " ) *> ( RejectContact <$> displayName )
2022-01-24 16:07:17 +00:00
<|> ( " /markdown " <|> " /m " ) $> ChatHelp HSMarkdown
2021-12-13 12:05:57 +00:00
<|> ( " /welcome " <|> " /w " ) $> Welcome
2022-04-04 19:51:49 +01:00
<|> " /profile_image " *> ( UpdateProfileImage . Just . ImageData <$> imageP )
2022-03-19 07:42:54 +00:00
<|> " /profile_image " $> UpdateProfileImage Nothing
2022-03-10 15:45:40 +04:00
<|> ( " /profile " <|> " /p " ) *> ( uncurry UpdateProfile <$> userNames )
2021-08-22 15:56:36 +01:00
<|> ( " /profile " <|> " /p " ) $> ShowProfile
2021-12-08 13:09:51 +00:00
<|> ( " /quit " <|> " /q " <|> " /exit " ) $> QuitChat
2021-11-07 21:57:05 +00:00
<|> ( " /version " <|> " /v " ) $> ShowVersion
2021-06-25 18:18:24 +01:00
where
2022-03-10 15:45:40 +04:00
imagePrefix = ( <> ) <$> " data: " <*> ( " image/png;base64, " <|> " image/jpg;base64, " )
imageP = safeDecodeUtf8 <$> ( ( <> ) <$> imagePrefix <*> ( B64 . encode <$> base64P ) )
2022-04-23 17:32:40 +01:00
chatTypeP = A . char '@' $> CTDirect <|> A . char '#' $> CTGroup <|> A . char ':' $> CTContactConnection
2022-02-01 15:05:27 +04:00
chatPaginationP =
( CPLast <$ " count= " <*> A . decimal )
<|> ( CPAfter <$ " after= " <*> A . decimal <* A . space <* " count= " <*> A . decimal )
<|> ( CPBefore <$ " before= " <*> A . decimal <* A . space <* " count= " <*> A . decimal )
2022-05-06 09:17:49 +01:00
mcTextP = MCText . safeDecodeUtf8 <$> A . takeByteString
msgContentP = " text " *> mcTextP <|> " json " *> jsonP
2022-03-28 20:35:57 +04:00
ciDeleteMode = " broadcast " $> CIDMBroadcast <|> " internal " $> CIDMInternal
2022-04-21 20:04:22 +01:00
tokenP = " apns " *> ( DeviceToken PPApns <$> hexStringP )
hexStringP =
A . takeWhile ( \ c -> ( c >= '0' && c <= '9' ) || ( c >= 'a' && c <= 'f' ) ) >>= \ s ->
if even ( B . length s ) then pure s else fail " odd number of hex characters "
2021-07-14 20:11:41 +01:00
displayName = safeDecodeUtf8 <$> ( B . cons <$> A . satisfy refChar <*> A . takeTill ( == ' ' ) )
2022-03-19 09:04:53 +00:00
sendMsgQuote msgDir = SendMessageQuote <$> displayName <* A . space <*> pure msgDir <*> quotedMsg <*> A . takeByteString
2022-03-13 19:34:03 +00:00
quotedMsg = A . char '(' *> A . takeTill ( == ')' ) <* A . char ')' <* optional A . space
2021-07-11 12:22:22 +01:00
refChar c = c > ' ' && c /= '#' && c /= '@'
2022-02-14 14:59:11 +04:00
onOffP = ( " on " $> True ) <|> ( " off " $> False )
2022-03-10 15:45:40 +04:00
userNames = do
2021-08-22 15:56:36 +01:00
cName <- displayName
fullName <- fullNameP cName
2022-03-10 15:45:40 +04:00
pure ( cName , fullName )
userProfile = do
( cName , fullName ) <- userNames
pure Profile { displayName = cName , fullName , image = Nothing }
2022-03-23 20:52:00 +00:00
jsonP :: J . FromJSON a => Parser a
jsonP = J . eitherDecodeStrict' <$?> A . takeByteString
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
2022-03-10 15:45:40 +04:00
pure GroupProfile { displayName = gName , fullName , image = Nothing }
2021-08-22 15:56:36 +01:00
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
2022-04-28 08:34:21 +01:00
chatNameP = ChatName <$> chatTypeP <*> displayName
2022-04-30 19:18:46 +04:00
chatNameP' = ChatName <$> ( chatTypeP <|> pure CTDirect ) <*> displayName
2022-04-28 08:34:21 +01:00
chatRefP = ChatRef <$> chatTypeP <*> A . decimal
2022-04-28 07:26:43 +01:00
msgCountP = A . space *> A . decimal <|> pure 10
2021-12-18 10:23:47 +00:00
2022-01-12 17:37:46 +00:00
adminContactReq :: ConnReqContact
adminContactReq =
either error id $ strDecode " https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D "