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 )
import Data.Attoparsec.ByteString.Char8 ( Parser )
2021-06-25 18:18:24 +01:00
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Bifunctor ( first )
import Data.ByteString.Char8 ( ByteString )
2021-07-11 12:22:22 +01:00
import qualified Data.ByteString.Char8 as B
2021-12-11 12:57:12 +00:00
import Data.Char ( isSpace )
2022-01-24 16:07:17 +00:00
import Data.Foldable ( for_ )
2021-06-25 18:18:24 +01:00
import Data.Functor ( ( $> ) )
2021-09-04 07:32:56 +01:00
import Data.Int ( Int64 )
2021-07-04 18:42:24 +01:00
import Data.List ( find )
2021-09-04 07:32:56 +01:00
import Data.Map.Strict ( Map )
import qualified Data.Map.Strict as M
2022-01-28 11:52:10 +04:00
import Data.Maybe ( isJust , mapMaybe )
2021-07-04 18:42:24 +01:00
import Data.Text ( Text )
2021-06-25 18:18:24 +01:00
import qualified Data.Text as T
2022-01-26 16:18:27 +04:00
import Data.Time.Clock ( UTCTime , getCurrentTime )
2022-01-28 10:41:09 +00:00
import Data.Time.LocalTime ( getCurrentTimeZone )
2022-01-11 12:41:38 +00:00
import Data.Word ( Word32 )
2021-06-25 18:18:24 +01:00
import Simplex.Chat.Controller
2022-01-24 16:07:17 +00:00
import Simplex.Chat.Messages
2021-07-07 22:46:38 +01:00
import Simplex.Chat.Options ( ChatOpts ( .. ) )
2021-07-04 18:42:24 +01:00
import Simplex.Chat.Protocol
2021-07-05 20:05:07 +01:00
import Simplex.Chat.Store
2021-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
2021-08-02 20:10:24 +01:00
import Simplex.Messaging.Agent.Env.SQLite ( AgentConfig ( .. ) , defaultAgentConfig )
2021-06-25 18:18:24 +01:00
import Simplex.Messaging.Agent.Protocol
2021-07-27 08:08:05 +01:00
import qualified Simplex.Messaging.Crypto as C
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
2021-06-25 18:18:24 +01:00
import Simplex.Messaging.Parsers ( parseAll )
2022-01-26 21:20:08 +00:00
import Simplex.Messaging.Protocol ( MsgBody )
2021-09-04 07:32:56 +01:00
import qualified Simplex.Messaging.Protocol as SMP
2022-01-24 16:07:17 +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-06 16:18:01 +00:00
import UnliftIO.Async ( Async , async , race_ )
2021-09-04 07:32:56 +01:00
import UnliftIO.Concurrent ( forkIO , threadDelay )
import UnliftIO.Directory ( doesDirectoryExist , doesFileExist , getFileSize , getHomeDirectory , getTemporaryDirectory )
2021-08-05 20:51:48 +01:00
import qualified UnliftIO.Exception as E
2021-09-04 07:32:56 +01:00
import UnliftIO.IO ( hClose , hSeek , hTell )
2021-06-25 18:18:24 +01:00
import UnliftIO.STM
2021-08-02 20:10:24 +01:00
defaultChatConfig :: ChatConfig
defaultChatConfig =
ChatConfig
{ agentConfig =
defaultAgentConfig
{ tcpPort = undefined , -- agent does not listen to TCP
smpServers = undefined , -- filled in from options
dbFile = undefined , -- filled in from options
dbPoolSize = 1
} ,
2021-07-24 10:26:28 +01:00
dbPoolSize = 1 ,
2021-09-04 07:32:56 +01:00
tbqSize = 16 ,
2022-01-12 06:07:49 +00:00
fileChunkSize = 15780
2021-07-07 22:46:38 +01:00
}
logCfg :: LogConfig
logCfg = LogConfig { lc_file = Nothing , lc_stderr = True }
2022-02-06 16:18:01 +00:00
newChatController :: SQLiteStore -> Maybe User -> ChatConfig -> ChatOpts -> ( Notification -> IO () ) -> IO ChatController
2022-01-21 11:09:33 +00:00
newChatController chatStore user config @ ChatConfig { agentConfig = cfg , tbqSize } ChatOpts { dbFilePrefix , smpServers } sendNotification = do
let f = chatStoreFile dbFilePrefix
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
smpAgent <- getSMPAgentClient cfg { dbFile = dbFilePrefix <> " _agent.db " , smpServers }
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-02-06 16:18:01 +00:00
pure ChatController { activeTo , firstTime , currentUser , smpAgent , agentAsync , chatStore , idsDrg , inputQ , outputQ , notifyQ , chatLock , sndFiles , rcvFiles , config , sendNotification }
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
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
execChatCommand s = case parseAll chatCommandP $ B . dropWhileEnd isSpace 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
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
StartChat -> withUser' $ \ user -> startChatController user $> CRChatStarted
APIGetChats -> CRApiChats <$> withUser ( \ user -> withStore ( ` getChatPreviews ` user ) )
APIGetChat 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 "
APIGetChatItems _pagination -> pure $ chatCmdError " not implemented "
APISendMessage cType chatId mc -> withUser $ \ user @ User { userId } -> withChatLock $ case cType of
2022-01-30 10:49:13 +00:00
CTDirect -> do
ct @ Contact { localDisplayName = c } <- withStore $ \ st -> getContact st userId chatId
ci <- sendDirectChatItem userId ct ( XMsgNew mc ) ( CISndMsgContent mc )
setActive $ ActiveC c
pure . CRNewChatItem $ AChatItem SCTDirect SMDSnd ( DirectChat ct ) ci
CTGroup -> do
group @ ( Group gInfo @ GroupInfo { localDisplayName = gName , membership } _ ) <- withStore $ \ st -> getGroup st user chatId
unless ( memberActive membership ) $ throwChatError CEGroupMemberUserRemoved
ci <- sendGroupChatItem userId group ( XMsgNew mc ) ( CISndMsgContent mc )
setActive $ ActiveG gName
pure . CRNewChatItem $ AChatItem SCTGroup SMDSnd ( GroupChat gInfo ) ci
2022-02-06 16:18:01 +00:00
CTContactRequest -> pure $ chatCmdError " not supported "
APIDeleteChat cType chatId -> withUser $ \ 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
conns <- withStore $ \ st -> getContactConnections st userId ct
2022-02-06 08:21:40 +00:00
withChatLock . procCmd $ do
2022-01-31 21:53:53 +04:00
withAgent $ \ a -> forM_ conns $ \ conn ->
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-02-06 16:18:01 +00:00
CTGroup -> pure $ chatCmdError " not implemented "
CTContactRequest -> pure $ chatCmdError " not supported "
APIAcceptContact connReqId -> withUser $ \ User { userId , profile } -> do
2022-02-01 17:04:44 +04:00
UserContactRequest { agentInvitationId = AgentInvId invId , localDisplayName = cName , profileId , profile = p } <- withStore $ \ st ->
2022-02-01 05:31:34 +00:00
getContactRequest st userId connReqId
2022-02-06 08:21:40 +00:00
withChatLock . procCmd $ do
2022-01-31 21:53:53 +04:00
connId <- withAgent $ \ a -> acceptContact a invId . directMessage $ XInfo profile
2022-02-01 17:04:44 +04:00
acceptedContact <- withStore $ \ st -> createAcceptedContact st userId connId cName profileId p
pure $ CRAcceptingContactRequest acceptedContact
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-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 )
2021-07-05 19:54:44 +01:00
withStore $ \ st -> createDirectConnection st userId connId
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
connect userId cReq $ XInfo profile
2022-01-24 16:07:17 +00:00
pure CRSentConfirmation
2022-02-06 16:18:01 +00:00
Connect ( Just ( ACR SCMContact cReq ) ) -> withUser $ \ User { userId , profile } -> withChatLock . procCmd $ do
connect userId cReq $ XContact profile Nothing
2022-01-24 16:07:17 +00:00
pure CRSentInvitation
2022-01-26 21:20:08 +00:00
Connect Nothing -> throwChatError CEInvalidConnReq
2022-02-06 16:18:01 +00:00
ConnectAdmin -> withUser $ \ User { userId , profile } -> withChatLock . procCmd $ do
connect userId adminContactReq $ XContact profile Nothing
2022-01-24 16:07:17 +00:00
pure CRSentInvitation
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-02-06 16:18:01 +00:00
processChatCommand $ APIDeleteChat CTDirect contactId
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-06 16:18:01 +00:00
ShowMyAddress -> CRUserContactLink <$> ( withUser $ \ User { userId } -> withStore ( ` getUserContactLink ` userId ) )
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
SendMessage cName msg -> withUser $ \ User { userId } -> do
2022-01-30 10:49:13 +00:00
contactId <- withStore $ \ st -> getContactIdByName st userId cName
2022-01-26 16:18:27 +04:00
let mc = MCText $ safeDecodeUtf8 msg
2022-02-06 16:18:01 +00:00
processChatCommand $ APISendMessage CTDirect contactId mc
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-01-24 16:07:17 +00:00
void . sendDirectMessage ( contactConn 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-01-24 16:07:17 +00:00
when ( mStatus /= GSMemInvited ) . void . sendGroupMessage members $ XGrpMemDel mId
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-01-24 16:07:17 +00:00
void $ sendGroupMessage members XGrpLeave
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-01-24 16:07:17 +00:00
when ( memberActive membership ) . void $ sendGroupMessage members XGrpDel
mapM_ deleteMemberConnection members
withStore $ \ st -> deleteGroup st user g
2022-01-26 16:18:27 +04:00
pure $ CRGroupDeletedUser gInfo
2022-02-06 16:18:01 +00:00
ListMembers gName -> CRGroupMembers <$> ( withUser $ \ user -> withStore ( \ st -> getGroupByName st user gName ) )
ListGroups -> CRGroupsList <$> withUser ( \ user -> withStore ( ` getUserGroupDetails ` user ) )
SendGroupMessage gName msg -> withUser $ \ user -> do
2022-01-30 10:49:13 +00:00
groupId <- withStore $ \ st -> getGroupIdByName st user gName
2022-01-26 16:18:27 +04:00
let mc = MCText $ safeDecodeUtf8 msg
2022-02-06 16:18:01 +00:00
processChatCommand $ APISendMessage CTGroup groupId mc
SendFile cName f -> withUser $ \ User { userId } -> withChatLock $ do
2021-09-05 14:08:29 +01:00
( fileSize , chSize ) <- checkSndFile f
2022-01-30 10:49:13 +00:00
contact <- withStore $ \ st -> getContactByName st userId cName
2021-12-08 13:09:51 +00:00
( agentConnId , fileConnReq ) <- withAgent ( ` createConnection ` SCMInvitation )
2021-12-02 11:17:09 +00:00
let fileInv = FileInvitation { fileName = takeFileName f , fileSize , fileConnReq }
2021-09-05 14:08:29 +01:00
SndFileTransfer { fileId } <- withStore $ \ st ->
createSndFileTransfer st userId contact f fileInv agentConnId chSize
2022-01-26 16:18:27 +04:00
ci <- sendDirectChatItem userId contact ( XFile fileInv ) ( CISndFileInvitation fileId f )
withStore $ \ st -> updateFileTransferChatItemId st fileId $ chatItemId ci
2021-09-04 07:32:56 +01:00
setActive $ ActiveC cName
2022-01-26 16:18:27 +04:00
pure . CRNewChatItem $ AChatItem SCTDirect SMDSnd ( DirectChat contact ) ci
2022-02-06 16:18:01 +00:00
SendGroupFile gName f -> withUser $ \ user @ User { userId } -> withChatLock $ do
2021-09-05 14:08:29 +01:00
( fileSize , chSize ) <- checkSndFile f
2022-01-30 10:49:13 +00:00
Group gInfo @ GroupInfo { membership } members <- withStore $ \ st -> getGroupByName st user gName
2022-01-26 21:20:08 +00:00
unless ( memberActive membership ) $ throwChatError CEGroupMemberUserRemoved
2021-09-05 14:08:29 +01:00
let fileName = takeFileName f
ms <- forM ( filter memberActive members ) $ \ m -> do
2021-12-08 13:09:51 +00:00
( connId , fileConnReq ) <- withAgent ( ` createConnection ` SCMInvitation )
2021-12-02 11:17:09 +00:00
pure ( m , connId , FileInvitation { fileName , fileSize , fileConnReq } )
2022-01-26 16:18:27 +04:00
fileId <- withStore $ \ st -> createSndGroupFileTransfer st userId gInfo ms f fileSize chSize
-- TODO sendGroupChatItem - same file invitation to all
2021-09-05 14:08:29 +01:00
forM_ ms $ \ ( m , _ , fileInv ) ->
2021-12-29 23:11:55 +04:00
traverse ( ` sendDirectMessage ` XFile fileInv ) $ memberConn m
2021-09-05 14:08:29 +01:00
setActive $ ActiveG gName
2022-01-24 16:07:17 +00:00
-- this is a hack as we have multiple direct messages instead of one per group
2022-01-26 16:18:27 +04:00
let ciContent = CISndFileInvitation fileId f
2022-01-28 11:52:10 +04:00
createdAt <- liftIO getCurrentTime
let ci = mkNewChatItem ciContent 0 createdAt createdAt
2022-01-28 10:41:09 +00:00
ciMeta @ CIMeta { itemId } <- saveChatItem userId ( CDGroupSnd gInfo ) ci
2022-01-26 16:18:27 +04:00
withStore $ \ st -> updateFileTransferChatItemId st fileId itemId
2022-01-28 10:41:09 +00:00
pure . CRNewChatItem $ AChatItem SCTGroup SMDSnd ( GroupChat gInfo ) $ ChatItem CIGroupSnd ciMeta ciContent
2022-02-06 16:18:01 +00:00
ReceiveFile fileId filePath_ -> withUser $ \ User { userId } -> do
2021-12-02 11:17:09 +00:00
ft @ RcvFileTransfer { fileInvitation = FileInvitation { fileName , fileConnReq } , fileStatus } <- withStore $ \ st -> getRcvFileTransfer st userId fileId
2022-01-26 21:20:08 +00:00
unless ( fileStatus == RFSNew ) . throwChatError $ CEFileAlreadyReceiving fileName
2022-02-06 08:21:40 +00:00
withChatLock . procCmd $ do
2022-01-24 16:07:17 +00:00
tryError ( withAgent $ \ a -> joinConnection a fileConnReq . directMessage $ XFileAcpt fileName ) >>= \ case
Right agentConnId -> do
filePath <- getRcvFilePath fileId filePath_ fileName
withStore $ \ st -> acceptRcvFileTransfer st userId fileId agentConnId filePath
pure $ CRRcvFileAccepted ft filePath
Left ( ChatErrorAgent ( SMP SMP . AUTH ) ) -> pure $ CRRcvFileAcceptedSndCancelled ft
Left ( ChatErrorAgent ( CONN DUPLICATE ) ) -> pure $ CRRcvFileAcceptedSndCancelled ft
Left e -> throwError e
2022-02-06 16:18:01 +00:00
CancelFile fileId -> withUser $ \ User { userId } -> do
2022-01-24 16:07:17 +00:00
ft' <- withStore ( \ st -> getFileTransfer st userId fileId )
2022-02-06 08:21:40 +00:00
withChatLock . procCmd $ case ft' of
2021-09-04 07:32:56 +01:00
FTSnd fts -> do
2021-09-05 14:08:29 +01:00
forM_ fts $ \ ft -> cancelSndFileTransfer ft
2022-01-24 16:07:17 +00:00
pure $ CRSndGroupFileCancelled fts
2021-09-04 07:32:56 +01:00
FTRcv ft -> do
cancelRcvFileTransfer ft
2022-01-24 16:07:17 +00:00
pure $ CRRcvFileCancelled ft
2021-09-04 07:32:56 +01:00
FileStatus fileId ->
2022-02-06 16:18:01 +00:00
CRFileTransferStatus <$> withUser ( \ User { userId } -> withStore $ \ st -> getFileTransferProgress st userId fileId )
ShowProfile -> withUser $ \ User { profile } -> pure $ CRUserProfile profile
UpdateProfile p @ Profile { displayName } -> withUser $ \ user @ User { profile } ->
if p == profile
then pure CRUserProfileNoChange
else do
withStore $ \ st -> updateUserProfile st user p
let user' = ( user :: User ) { localDisplayName = displayName , profile = p }
asks currentUser >>= atomically . ( ` writeTVar ` Just user' )
contacts <- withStore ( ` getUserContacts ` user )
withChatLock . procCmd $ do
forM_ contacts $ \ ct -> sendDirectMessage ( contactConn ct ) $ XInfo p
pure $ CRUserProfileUpdated profile p
2021-07-27 08:08:05 +01:00
QuitChat -> liftIO exitSuccess
2022-01-24 16:07:17 +00:00
ShowVersion -> pure CRVersionInfo
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-02 17:47:27 +00:00
-- procCmd :: m ChatResponse -> m ChatResponse
2022-02-04 08:02:48 +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 $
2022-02-06 16:18:01 +00:00
-- (atomically . writeTBQueue q) . (Just corrId,) =<< (action `catchError` (pure . CRChatCmdError))
2022-02-04 08:02:48 +00:00
-- pure $ CRCmdAccepted corrId
-- use function below to make commands "synchronous"
procCmd :: m ChatResponse -> m ChatResponse
procCmd = id
2022-02-06 16:18:01 +00:00
connect :: UserId -> ConnectionRequestUri c -> ChatMsgEvent -> m ()
connect userId cReq msg = do
2021-12-11 12:57:12 +00:00
connId <- withAgent $ \ a -> joinConnection a cReq $ directMessage msg
withStore $ \ st -> createDirectConnection st userId connId
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-01-26 21:20:08 +00:00
unlessM ( doesFileExist f ) . throwChatError $ CEFileNotFound f
2021-09-05 14:08:29 +01:00
( , ) <$> getFileSize f <*> asks ( fileChunkSize . config )
2021-09-04 07:32:56 +01:00
getRcvFilePath :: Int64 -> Maybe FilePath -> String -> m FilePath
getRcvFilePath fileId filePath fileName = case filePath of
Nothing -> do
dir <- ( ` combine ` " Downloads " ) <$> getHomeDirectory
ifM ( doesDirectoryExist dir ) ( pure dir ) getTemporaryDirectory
>>= ( ` uniqueCombine ` fileName )
>>= createEmptyFile
Just fPath ->
ifM
( doesDirectoryExist fPath )
( fPath ` uniqueCombine ` fileName >>= createEmptyFile )
$ ifM
( doesFileExist fPath )
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
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-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-02-06 16:18:01 +00:00
subscribeUserConnections 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 )
subscribeUserConnections :: ( MonadUnliftIO m , MonadReader ChatController m ) => User -> m ()
subscribeUserConnections user @ User { userId } = void . runExceptT $ do
subscribeContacts
subscribeGroups
subscribeFiles
subscribePendingConnections
subscribeUserContactLink
2021-07-25 20:23:52 +01:00
where
2022-02-06 16:18:01 +00:00
subscribeContacts = do
2021-07-25 20:23:52 +01:00
contacts <- withStore ( ` getUserContacts ` user )
2022-01-26 16:18:27 +04:00
forM_ contacts $ \ ct ->
( subscribe ( contactConnId ct ) >> toView ( CRContactSubscribed ct ) ) ` catchError ` ( toView . CRContactSubError ct )
2022-02-06 16:18:01 +00:00
subscribeGroups = do
2021-08-02 20:10:24 +01:00
groups <- withStore ( ` getUserGroups ` user )
2022-01-26 16:18:27 +04:00
forM_ groups $ \ ( Group g @ GroupInfo { membership } members ) -> do
2021-07-25 20:23:52 +01:00
let connectedMembers = mapMaybe ( \ m -> ( m , ) <$> memberConnId m ) members
2022-01-06 13:09:03 +04:00
if memberStatus membership == GSMemInvited
2022-01-24 16:07:17 +00:00
then toView $ CRGroupInvitation g
2022-01-06 13:09:03 +04:00
else
if null connectedMembers
then
if memberActive membership
2022-01-24 16:07:17 +00:00
then toView $ CRGroupEmpty g
else toView $ CRGroupRemoved g
2022-01-06 13:09:03 +04:00
else do
forM_ connectedMembers $ \ ( GroupMember { localDisplayName = c } , cId ) ->
2022-01-26 16:18:27 +04:00
subscribe cId ` catchError ` ( toView . CRMemberSubError g c )
2022-01-24 16:07:17 +00:00
toView $ CRGroupSubscribed g
2022-02-06 16:18:01 +00:00
subscribeFiles = do
2021-09-04 07:32:56 +01:00
withStore ( ` getLiveSndFileTransfers ` user ) >>= mapM_ subscribeSndFile
withStore ( ` getLiveRcvFileTransfers ` user ) >>= mapM_ subscribeRcvFile
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
unless ( fileStatus == FSNew ) . unlessM ( isFileActive fileId sndFiles ) $
withAgentLock a . withLock l $
2022-01-24 16:07:17 +00:00
sendFileChunk 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-06 16:18:01 +00:00
subscribePendingConnections = do
2021-12-08 13:09:51 +00:00
cs <- withStore ( ` getPendingConnections ` user )
subscribeConns cs ` catchError ` \ _ -> pure ()
2022-02-06 16:18:01 +00:00
subscribeUserContactLink = do
2021-12-08 13:09:51 +00:00
cs <- withStore ( ` getUserContactLinkConnections ` userId )
2022-01-24 16:07:17 +00:00
( subscribeConns cs >> toView CRUserContactLinkSubscribed )
` catchError ` ( toView . CRUserContactLinkSubError )
2021-07-25 20:23:52 +01:00
subscribe cId = withAgent ( ` subscribeConnection ` cId )
2021-12-08 13:09:51 +00:00
subscribeConns conns =
withAgent $ \ a ->
2022-01-26 21:20:08 +00:00
forM_ conns $ subscribeConnection a . aConnId
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
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
contactIsReady :: Contact -> Bool
contactIsReady Contact { activeConn } = connStatus activeConn == ConnReady
memberIsReady :: GroupMember -> Bool
memberIsReady GroupMember { activeConn } = maybe False ( ( == ConnReady ) . connStatus ) activeConn
agentMsgConnStatus :: ACommand 'Agent -> Maybe ConnStatus
agentMsgConnStatus = \ case
2021-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 ()
processDirectMessage agentMsg conn = \ case
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
_ <- saveRcvMSG conn 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 ->
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 18:11:04 +01:00
Just ct @ Contact { localDisplayName = c } -> case agentMsg of
2022-01-26 16:18:27 +04:00
MSG msgMeta msgBody -> do
( msgId , chatMsgEvent ) <- saveRcvMSG conn msgMeta msgBody
withAckMessage agentConnId msgMeta $
2021-12-29 23:11:55 +04:00
case chatMsgEvent of
2022-01-26 16:18:27 +04:00
XMsgNew mc -> newContentMessage ct mc msgId msgMeta
XFile fInv -> processFileInvitation ct fInv msgId 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
_ -> 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-01-26 16:18:27 +04:00
Just ( gInfo , m ) -> do
2021-07-27 08:08:05 +01:00
when ( memberIsReady m ) $ 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
2021-12-29 23:11:55 +04:00
SENT msgId ->
sentMsgDeliveryEvent conn msgId
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
DOWN -> do
2022-01-26 16:18:27 +04:00
toView $ CRContactDisconnected ct
2021-07-24 10:26:28 +01:00
showToast ( c <> " > " ) " disconnected "
2021-08-14 21:04:51 +01:00
UP -> do
2022-01-26 16:18:27 +04:00
toView $ CRContactSubscribed ct
2021-08-14 21:04:51 +01:00
showToast ( c <> " > " ) " is active "
setActive $ ActiveC c
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
2022-01-26 16:18:27 +04:00
processGroupMessage :: ACommand 'Agent -> Connection -> GroupInfo -> GroupMember -> m ()
processGroupMessage agentMsg conn gInfo @ GroupInfo { 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-01-24 16:07:17 +00:00
void . sendGroupMessage members . XGrpMemNew $ memberInfo m
forM_ intros $ \ intro @ GroupMemberIntro { introId } -> do
void . sendDirectMessage conn . XGrpMemIntro . memberInfo $ reMember intro
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 "
Just ct ->
2021-07-27 08:08:05 +01:00
when ( contactIsReady ct ) $ 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
( msgId , chatMsgEvent ) <- saveRcvMSG conn msgMeta msgBody
withAckMessage agentConnId msgMeta $
2021-12-29 23:11:55 +04:00
case chatMsgEvent of
2022-01-26 16:18:27 +04:00
XMsgNew mc -> newGroupContentMessage gInfo m mc msgId msgMeta
XFile fInv -> processGroupFileInvitation gInfo m fInv msgId msgMeta
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
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
withStore $ \ st -> updateSndFileStatus st ft FSConnected
2022-01-24 16:07:17 +00:00
toView $ CRSndFileStart ft
sendFileChunk ft
2021-09-04 07:32:56 +01:00
SENT msgId -> do
withStore $ \ st -> updateSndFileChunkSent st ft msgId
2022-01-24 16:07:17 +00:00
unless ( fileStatus == FSCancelled ) $ sendFileChunk ft
2021-09-04 07:32:56 +01:00
MERR _ err -> do
cancelSndFileTransfer ft
case err of
2022-01-24 16:07:17 +00:00
SMP SMP . AUTH -> unless ( fileStatus == FSCancelled ) $ toView $ CRSndFileRcvCancelled 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 ()
processRcvFileConn agentMsg _conn ft @ RcvFileTransfer { fileId , chunkSize } =
case agentMsg of
CON -> do
withStore $ \ st -> updateRcvFileStatus st ft FSConnected
2022-01-24 16:07:17 +00:00
toView $ CRRcvFileStart ft
2021-09-04 07:32:56 +01:00
MSG meta @ MsgMeta { recipient = ( msgId , _ ) , integrity } msgBody -> withAckMessage agentConnId meta $ do
parseFileChunk msgBody >>= \ case
2022-01-11 12:41:38 +00:00
FileChunkCancel -> do
2021-09-04 07:32:56 +01:00
cancelRcvFileTransfer ft
2022-01-24 16:07:17 +00:00
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
2021-09-05 14:08:29 +01:00
withStore $ \ st -> do
updateRcvFileStatus st ft FSComplete
deleteRcvFileChunks st ft
2022-01-24 16:07:17 +00:00
toView $ CRRcvFileComplete ft
2021-09-04 07:32:56 +01:00
closeFileHandle fileId rcvFiles
withAgent ( ` deleteConnection ` agentConnId )
RcvChunkDuplicate -> pure ()
RcvChunkError -> badRcvFileChunk ft $ " incorrect chunk number " <> show chunkNo
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
XContact p _ -> profileContactRequest invId p
XInfo p -> profileContactRequest invId p
-- 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
profileContactRequest :: InvitationId -> Profile -> m ()
profileContactRequest invId p = do
2022-01-31 21:53:53 +04:00
cReq @ UserContactRequest { localDisplayName } <- withStore $ \ st -> createContactRequest st userId userContactLinkId invId p
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
2021-09-04 07:32:56 +01:00
badRcvFileChunk :: RcvFileTransfer -> String -> m ()
badRcvFileChunk ft @ RcvFileTransfer { fileStatus } err =
case fileStatus of
RFSCancelled _ -> pure ()
_ -> do
cancelRcvFileTransfer ft
2022-01-26 21:20:08 +00:00
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-01-24 16:07:17 +00:00
void . sendDirectMessage ( contactConn 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-01-24 16:07:17 +00:00
void . sendDirectMessage ( contactConn 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-01-26 16:18:27 +04:00
newContentMessage :: Contact -> MsgContent -> MessageId -> MsgMeta -> m ()
newContentMessage ct @ Contact { localDisplayName = c } mc msgId msgMeta = do
2022-01-28 10:41:09 +00:00
ci <- saveRcvDirectChatItem userId ct msgId msgMeta ( CIRcvMsgContent mc )
2022-01-26 16:18:27 +04:00
toView . CRNewChatItem $ AChatItem SCTDirect SMDRcv ( DirectChat ct ) ci
2022-02-02 11:43:52 +00:00
checkIntegrity msgMeta $ toView . CRMsgIntegrityError
2022-01-24 16:07:17 +00:00
showToast ( c <> " > " ) $ msgContentText mc
2022-01-11 08:50:44 +00:00
setActive $ ActiveC c
2022-01-26 16:18:27 +04:00
newGroupContentMessage :: GroupInfo -> GroupMember -> MsgContent -> MessageId -> MsgMeta -> m ()
newGroupContentMessage gInfo m @ GroupMember { localDisplayName = c } mc msgId msgMeta = do
2022-01-28 10:41:09 +00:00
ci <- saveRcvGroupChatItem userId gInfo m msgId msgMeta ( CIRcvMsgContent mc )
2022-01-26 16:18:27 +04:00
toView . CRNewChatItem $ AChatItem SCTGroup SMDRcv ( GroupChat gInfo ) ci
2022-02-02 11:43:52 +00:00
checkIntegrity msgMeta $ toView . CRMsgIntegrityError
2022-01-27 22:01:15 +00:00
let g = groupName' gInfo
2022-01-26 16:18:27 +04:00
showToast ( " # " <> g <> " " <> c <> " > " ) $ msgContentText mc
setActive $ ActiveG g
2021-07-07 22:46:38 +01:00
2022-01-26 16:18:27 +04:00
processFileInvitation :: Contact -> FileInvitation -> MessageId -> MsgMeta -> m ()
processFileInvitation ct @ Contact { localDisplayName = c } fInv msgId msgMeta = do
2021-09-04 07:32:56 +01:00
-- TODO chunk size has to be sent as part of invitation
chSize <- asks $ fileChunkSize . config
2022-01-26 16:18:27 +04:00
ft @ RcvFileTransfer { fileId } <- withStore $ \ st -> createRcvFileTransfer st userId ct fInv chSize
ci <- saveRcvDirectChatItem userId ct msgId msgMeta ( CIRcvFileInvitation ft )
withStore $ \ st -> updateFileTransferChatItemId st fileId $ chatItemId ci
toView . CRNewChatItem $ AChatItem SCTDirect SMDRcv ( DirectChat ct ) ci
2022-02-02 11:43:52 +00:00
checkIntegrity msgMeta $ toView . CRMsgIntegrityError
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-01-26 16:18:27 +04:00
processGroupFileInvitation :: GroupInfo -> GroupMember -> FileInvitation -> MessageId -> MsgMeta -> m ()
processGroupFileInvitation gInfo m @ GroupMember { localDisplayName = c } fInv msgId msgMeta = do
2021-09-05 14:08:29 +01:00
chSize <- asks $ fileChunkSize . config
2022-01-26 16:18:27 +04:00
ft @ RcvFileTransfer { fileId } <- withStore $ \ st -> createRcvGroupFileTransfer st userId m fInv chSize
ci <- saveRcvGroupChatItem userId gInfo m msgId msgMeta ( CIRcvFileInvitation ft )
withStore $ \ st -> updateFileTransferChatItemId st fileId $ chatItemId ci
toView . CRNewChatItem $ AChatItem SCTGroup SMDRcv ( GroupChat gInfo ) ci
2022-02-02 11:43:52 +00:00
checkIntegrity msgMeta $ toView . CRMsgIntegrityError
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
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-01-24 16:07:17 +00:00
void . sendDirectMessage ( contactConn 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
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
2021-07-11 12:22:22 +01:00
XInfo p ->
withStore $ \ st -> createDirectContact st userId activeConn p
2021-07-24 18:11:04 +01:00
-- TODO show/log error, other events in SMP confirmation
_ -> pure ()
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 ()
xGrpMemIntro conn gInfo 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-01-24 16:07:17 +00:00
void $ sendDirectMessage conn msg
2021-08-02 20:10:24 +01:00
withStore $ \ st -> updateGroupMemberStatus st userId newMember GSMemIntroInvited
2021-07-24 18:11:04 +01:00
_ -> messageError " x.grp.mem.intro can be only sent by host member "
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
2021-07-24 18:11:04 +01:00
Nothing -> messageError " x.grp.mem.inv error: referenced member does not exists "
Just reMember -> do
2022-01-24 16:07:17 +00:00
GroupMemberIntro { introId } <- withStore $ \ st -> saveIntroInvitation st reMember m introInv
void $ sendXGrpMemInv 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-01-24 16:07:17 +00:00
sendFileChunk :: ChatMonad m => SndFileTransfer -> m ()
2022-01-26 16:18:27 +04:00
sendFileChunk 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
2021-09-05 14:08:29 +01:00
withStore $ \ st -> do
updateSndFileStatus st ft FSComplete
deleteSndFileChunks st ft
2022-01-24 16:07:17 +00:00
toView $ CRSndFileComplete 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
readFileChunk SndFileTransfer { fileId , filePath , chunkSize } chunkNo =
2022-01-26 21:20:08 +00:00
read_ ` E . catch ` ( throwChatError . CEFileRead filePath . ( show :: E . SomeException -> String ) )
2021-09-04 07:32:56 +01:00
where
read_ = do
h <- getFileHandle fileId filePath sndFiles ReadMode
pos <- hTell h
let pos' = ( chunkNo - 1 ) * chunkSize
when ( pos /= pos' ) $ hSeek h AbsoluteSeek pos'
liftIO . B . hGet h $ fromInteger chunkSize
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
RFSConnected RcvFileInfo { filePath } -> append_ filePath
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
append_ fPath = do
h <- getFileHandle fileId fPath rcvFiles AppendMode
E . try ( liftIO $ B . hPut h chunk >> hFlush h ) >>= \ case
2022-01-26 21:20:08 +00:00
Left ( e :: E . SomeException ) -> throwChatError . CEFileWrite fPath $ 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
cancelRcvFileTransfer :: ChatMonad m => RcvFileTransfer -> m ()
cancelRcvFileTransfer ft @ RcvFileTransfer { fileId , fileStatus } = do
closeFileHandle fileId rcvFiles
2021-09-05 14:08:29 +01:00
withStore $ \ st -> do
updateRcvFileStatus st ft FSCancelled
deleteRcvFileChunks st ft
2021-09-04 07:32:56 +01:00
case fileStatus of
2022-01-26 16:18:27 +04:00
RFSAccepted RcvFileInfo { agentConnId = AgentConnId acId } -> withAgent ( ` suspendConnection ` acId )
RFSConnected RcvFileInfo { agentConnId = AgentConnId acId } -> withAgent ( ` suspendConnection ` acId )
2021-09-04 07:32:56 +01:00
_ -> pure ()
cancelSndFileTransfer :: ChatMonad m => SndFileTransfer -> m ()
2022-01-26 16:18:27 +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 ()
suspendConnection 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
withAgent $ forM_ ( memberConnId m ) . suspendConnection
-- withStore $ \st -> deleteGroupMemberConnection st userId m
forM_ activeConn $ \ conn -> withStore $ \ st -> updateConnectionStatus st conn ConnDeleted
2021-08-02 20:10:24 +01:00
2022-01-26 16:18:27 +04:00
sendDirectMessage :: ChatMonad m => Connection -> ChatMsgEvent -> m MessageId
2021-12-29 23:11:55 +04:00
sendDirectMessage conn chatMsgEvent = do
2022-01-26 16:18:27 +04:00
( msgId , msgBody ) <- createSndMessage chatMsgEvent
2021-12-29 23:11:55 +04:00
deliverMessage conn msgBody msgId
2022-01-26 16:18:27 +04:00
pure msgId
2022-01-24 16:07:17 +00:00
2022-01-26 16:18:27 +04:00
createSndMessage :: ChatMonad m => ChatMsgEvent -> m ( MessageId , MsgBody )
2022-01-24 16:07:17 +00:00
createSndMessage chatMsgEvent = do
let msgBody = directMessage chatMsgEvent
2022-01-26 16:18:27 +04:00
newMsg = NewMessage { direction = MDSnd , cmEventTag = toCMEventTag chatMsgEvent , msgBody }
msgId <- withStore $ \ st -> createNewMessage st newMsg
pure ( msgId , msgBody )
2021-07-16 07:40:55 +01:00
directMessage :: ChatMsgEvent -> ByteString
2022-01-11 08:50:44 +00:00
directMessage chatMsgEvent = strEncode ChatMessage { 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-01-26 16:18:27 +04:00
sendGroupMessage :: ChatMonad m => [ GroupMember ] -> ChatMsgEvent -> m MessageId
2022-01-24 16:07:17 +00:00
sendGroupMessage members chatMsgEvent =
sendGroupMessage' members chatMsgEvent Nothing $ pure ()
2022-01-26 16:18:27 +04:00
sendXGrpMemInv :: ChatMonad m => GroupMember -> ChatMsgEvent -> Int64 -> m MessageId
2022-01-24 16:07:17 +00:00
sendXGrpMemInv reMember chatMsgEvent introId =
sendGroupMessage' [ reMember ] chatMsgEvent ( Just introId ) $
withStore ( \ st -> updateIntroStatus st introId GMIntroInvForwarded )
2022-01-26 16:18:27 +04:00
sendGroupMessage' :: ChatMonad m => [ GroupMember ] -> ChatMsgEvent -> Maybe Int64 -> m () -> m MessageId
2022-01-24 16:07:17 +00:00
sendGroupMessage' members chatMsgEvent introId_ postDeliver = do
2022-01-26 16:18:27 +04:00
( msgId , msgBody ) <- createSndMessage chatMsgEvent
2022-01-24 16:07:17 +00:00
for_ ( filter memberCurrent members ) $ \ m @ GroupMember { groupMemberId } ->
case memberConn m of
Nothing -> withStore $ \ st -> createPendingGroupMessage st groupMemberId msgId introId_
Just conn -> deliverMessage conn msgBody msgId >> postDeliver
2022-01-26 16:18:27 +04:00
pure msgId
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
for_ pendingMessages $ \ PendingGroupMessage { msgId , cmEventTag , msgBody , introId_ } -> do
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-01-26 16:18:27 +04:00
saveRcvMSG :: ChatMonad m => Connection -> MsgMeta -> MsgBody -> m ( MessageId , ChatMsgEvent )
2021-12-29 23:11:55 +04:00
saveRcvMSG Connection { connId } agentMsgMeta msgBody = do
ChatMessage { chatMsgEvent } <- liftEither $ parseChatMessage msgBody
2022-01-24 16:07:17 +00:00
let agentMsgId = fst $ recipient agentMsgMeta
cmEventTag = toCMEventTag chatMsgEvent
2022-01-26 16:18:27 +04:00
newMsg = NewMessage { direction = MDRcv , cmEventTag , msgBody }
2021-12-29 23:11:55 +04:00
rcvMsgDelivery = RcvMsgDelivery { connId , agentMsgId , agentMsgMeta }
2022-01-26 16:18:27 +04:00
msgId <- withStore $ \ st -> createNewMessageAndRcvMsgDelivery st newMsg rcvMsgDelivery
pure ( msgId , chatMsgEvent )
sendDirectChatItem :: ChatMonad m => UserId -> Contact -> ChatMsgEvent -> CIContent 'MDSnd -> m ( ChatItem 'CTDirect 'MDSnd )
sendDirectChatItem userId contact @ Contact { activeConn } chatMsgEvent ciContent = do
msgId <- sendDirectMessage activeConn chatMsgEvent
2022-01-28 11:52:10 +04:00
createdAt <- liftIO getCurrentTime
2022-01-28 10:41:09 +00:00
ciMeta <- saveChatItem userId ( CDDirectSnd contact ) $ mkNewChatItem ciContent msgId createdAt createdAt
pure $ ChatItem CIDirectSnd ciMeta ciContent
2022-01-26 16:18:27 +04:00
sendGroupChatItem :: ChatMonad m => UserId -> Group -> ChatMsgEvent -> CIContent 'MDSnd -> m ( ChatItem 'CTGroup 'MDSnd )
sendGroupChatItem userId ( Group g ms ) chatMsgEvent ciContent = do
msgId <- sendGroupMessage ms chatMsgEvent
2022-01-28 11:52:10 +04:00
createdAt <- liftIO getCurrentTime
2022-01-28 10:41:09 +00:00
ciMeta <- saveChatItem userId ( CDGroupSnd g ) $ mkNewChatItem ciContent msgId createdAt createdAt
pure $ ChatItem CIGroupSnd ciMeta ciContent
2022-01-26 16:18:27 +04:00
saveRcvDirectChatItem :: ChatMonad m => UserId -> Contact -> MessageId -> MsgMeta -> CIContent 'MDRcv -> m ( ChatItem 'CTDirect 'MDRcv )
2022-01-28 11:52:10 +04:00
saveRcvDirectChatItem userId ct msgId MsgMeta { broker = ( _ , brokerTs ) } ciContent = do
createdAt <- liftIO getCurrentTime
2022-01-28 10:41:09 +00:00
ciMeta <- saveChatItem userId ( CDDirectRcv ct ) $ mkNewChatItem ciContent msgId brokerTs createdAt
pure $ ChatItem CIDirectRcv ciMeta ciContent
2022-01-26 16:18:27 +04:00
saveRcvGroupChatItem :: ChatMonad m => UserId -> GroupInfo -> GroupMember -> MessageId -> MsgMeta -> CIContent 'MDRcv -> m ( ChatItem 'CTGroup 'MDRcv )
2022-01-28 11:52:10 +04:00
saveRcvGroupChatItem userId g m msgId MsgMeta { broker = ( _ , brokerTs ) } ciContent = do
createdAt <- liftIO getCurrentTime
2022-01-28 10:41:09 +00:00
ciMeta <- saveChatItem userId ( CDGroupRcv g m ) $ mkNewChatItem ciContent msgId brokerTs createdAt
pure $ ChatItem ( CIGroupRcv m ) ciMeta ciContent
2022-01-28 11:52:10 +04:00
2022-01-28 10:41:09 +00:00
saveChatItem :: ChatMonad m => UserId -> ChatDirection c d -> NewChatItem d -> m CIMeta
2022-01-28 11:52:10 +04:00
saveChatItem userId cd ci @ NewChatItem { itemTs , itemText , createdAt } = do
2022-01-28 10:41:09 +00:00
tz <- liftIO getCurrentTimeZone
2022-01-28 11:52:10 +04:00
ciId <- withStore $ \ st -> createNewChatItem st userId cd ci
2022-01-28 10:41:09 +00:00
pure $ mkCIMeta ciId itemText tz itemTs createdAt
2022-01-28 11:52:10 +04:00
mkNewChatItem :: forall d . MsgDirectionI d => CIContent d -> MessageId -> UTCTime -> UTCTime -> NewChatItem d
mkNewChatItem itemContent msgId itemTs createdAt =
NewChatItem
{ createdByMsgId = if msgId == 0 then Nothing else Just msgId ,
itemSent = msgDirection @ d ,
itemTs ,
itemContent ,
itemText = ciContentToText itemContent ,
createdAt
}
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) "
liftIO ( runExceptT $ createUser st Profile { displayName , fullName } True ) >>= \ case
Left SEDuplicateName -> do
putStrLn " chosen display name is already used by another profile on this device, choose another one "
2021-07-05 19:54:44 +01:00
loop
Left e -> putStrLn ( " database error " <> show e ) >> exitFailure
Right user -> pure user
selectUser :: [ User ] -> IO User
selectUser [ user ] = do
liftIO $ setActiveUser st ( userId user )
pure user
selectUser users = do
2021-07-05 20:05:07 +01:00
putStrLn " Select user profile: "
2021-07-05 19:54:44 +01:00
forM_ ( zip [ 1 .. ] users ) $ \ ( n :: Int , user ) -> putStrLn $ show n <> " - " <> userStr user
loop
where
loop = do
nStr <- getWithPrompt $ " user profile number (1 .. " <> show ( length users ) <> " ) "
case readMaybe nStr :: Maybe Int of
Nothing -> putStrLn " invalid user number " >> loop
Just n
| n <= 0 || n > length users -> putStrLn " invalid user number " >> loop
| otherwise -> do
let user = users !! ( n - 1 )
liftIO $ setActiveUser st ( userId user )
pure user
userStr :: User -> String
2021-07-14 20:11:41 +01:00
userStr User { localDisplayName , profile = Profile { fullName } } =
2021-08-22 15:56:36 +01:00
T . unpack $ localDisplayName <> if T . null fullName || localDisplayName == fullName then " " else " ( " <> fullName <> " ) "
2021-07-14 20:11:41 +01:00
getContactName :: IO ContactName
getContactName = do
displayName <- getWithPrompt " display name (no spaces) "
if null displayName || isJust ( find ( == ' ' ) displayName )
then putStrLn " display name has space(s), choose another one " >> getContactName
else pure $ T . pack displayName
2021-07-05 19:54:44 +01:00
getWithPrompt :: String -> IO String
getWithPrompt s = putStr ( s <> " : " ) >> hFlush stdout >> getLine
2021-06-25 18:18:24 +01:00
2021-07-04 18:42:24 +01:00
showToast :: ( MonadUnliftIO m , MonadReader ChatController m ) => Text -> Text -> m ()
showToast title text = atomically . ( ` writeTBQueue ` Notification { title , text } ) =<< asks notifyQ
2021-06-26 20:20:33 +01:00
notificationSubscriber :: ( MonadUnliftIO m , MonadReader ChatController m ) => m ()
notificationSubscriber = do
ChatController { notifyQ , sendNotification } <- ask
forever $ atomically ( readTBQueue notifyQ ) >>= liftIO . sendNotification
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
>>= runExceptT . action
>>= liftEither . first ChatErrorStore
2021-07-04 18:42:24 +01:00
2021-06-25 18:18:24 +01:00
chatCommandP :: Parser ChatCommand
chatCommandP =
2022-02-06 16:18:01 +00:00
( " /user " <|> " /u " ) *> ( CreateActiveUser <$> userProfile )
<|> ( " /user " <|> " /u " ) $> ShowActiveUser
<|> " /_start " $> StartChat
<|> " /_get chats " $> APIGetChats
2022-02-01 15:05:27 +04:00
<|> " /_get chat " *> ( APIGetChat <$> chatTypeP <*> A . decimal <* A . space <*> chatPaginationP )
2022-01-31 23:20:52 +00:00
<|> " /_get items count= " *> ( APIGetChatItems <$> A . decimal )
<|> " /_send " *> ( APISendMessage <$> chatTypeP <*> A . decimal <* A . space <*> msgContentP )
<|> " /_delete " *> ( APIDeleteChat <$> chatTypeP <*> A . decimal )
<|> " /_accept " *> ( APIAcceptContact <$> A . decimal )
2022-02-01 05:31:34 +00:00
<|> " /_reject " *> ( APIRejectContact <$> A . decimal )
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
<|> ( " /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 )
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
2021-07-14 20:11:41 +01:00
<|> A . char '#' *> ( SendGroupMessage <$> displayName <* A . space <*> 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
2021-07-14 20:11:41 +01:00
<|> ( " /delete @ " <|> " /delete " <|> " /d @ " <|> " /d " ) *> ( DeleteContact <$> displayName )
2022-01-30 10:49:13 +00:00
<|> A . char '@' *> ( SendMessage <$> displayName <* A . space <*> A . takeByteString )
2021-09-04 07:32:56 +01:00
<|> ( " /file # " <|> " /f # " ) *> ( SendGroupFile <$> displayName <* A . space <*> filePath )
<|> ( " /file @ " <|> " /file " <|> " /f @ " <|> " /f " ) *> ( SendFile <$> displayName <* A . space <*> filePath )
2021-09-05 05:38:11 +10:00
<|> ( " /freceive " <|> " /fr " ) *> ( ReceiveFile <$> A . decimal <*> optional ( A . space *> filePath ) )
<|> ( " /fcancel " <|> " /fc " ) *> ( CancelFile <$> A . decimal )
<|> ( " /fstatus " <|> " /fs " ) *> ( FileStatus <$> A . decimal )
2022-01-12 17:37:46 +00:00
<|> " /simplex " $> ConnectAdmin
2021-12-08 13:09:51 +00:00
<|> ( " /address " <|> " /ad " ) $> CreateMyAddress
<|> ( " /delete_address " <|> " /da " ) $> DeleteMyAddress
<|> ( " /show_address " <|> " /sa " ) $> ShowMyAddress
<|> ( " /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
2021-08-22 15:56:36 +01:00
<|> ( " /profile " <|> " /p " ) *> ( UpdateProfile <$> userProfile )
<|> ( " /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-02-01 05:31:34 +00:00
chatTypeP = A . char '@' $> CTDirect <|> A . char '#' $> CTGroup
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-01-30 10:49:13 +00:00
msgContentP = " text " *> ( MCText . safeDecodeUtf8 <$> A . takeByteString )
2021-07-14 20:11:41 +01:00
displayName = safeDecodeUtf8 <$> ( B . cons <$> A . satisfy refChar <*> A . takeTill ( == ' ' ) )
2021-07-11 12:22:22 +01:00
refChar c = c > ' ' && c /= '#' && c /= '@'
2021-08-22 15:56:36 +01:00
userProfile = do
cName <- displayName
fullName <- fullNameP cName
pure Profile { displayName = cName , fullName }
2021-07-12 19:00:03 +01:00
groupProfile = do
2021-07-16 07:40:55 +01:00
gName <- displayName
2021-08-22 15:56:36 +01:00
fullName <- fullNameP gName
pure GroupProfile { displayName = gName , fullName }
fullNameP name = do
n <- ( A . space *> A . takeByteString ) <|> pure " "
pure $ if B . null n then name else safeDecodeUtf8 n
2021-09-04 07:32:56 +01:00
filePath = T . unpack . safeDecodeUtf8 <$> A . takeByteString
2021-07-11 12:22:22 +01:00
memberRole =
2021-07-12 19:00:03 +01:00
( " owner " $> GROwner )
<|> ( " admin " $> GRAdmin )
2021-09-05 05:38:11 +10:00
<|> ( " member " $> GRMember )
2021-07-24 10:26:28 +01:00
<|> pure GRAdmin
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 "