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 # -}
2021-06-25 18:18:24 +01:00
module Simplex.Chat where
2021-07-05 19:54:44 +01:00
import Control.Applicative ( ( <|> ) )
2021-07-07 22:46:38 +01:00
import Control.Logger.Simple
2021-06-25 18:18:24 +01:00
import Control.Monad.Except
import Control.Monad.IO.Unlift
import Control.Monad.Reader
2021-07-12 19:00:03 +01:00
import Crypto.Random ( drgNew )
import Data.Attoparsec.ByteString.Char8 ( Parser )
2021-06-25 18:18:24 +01:00
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Bifunctor ( first )
import Data.ByteString.Char8 ( ByteString )
2021-07-11 12:22:22 +01:00
import qualified Data.ByteString.Char8 as B
2021-06-25 18:18:24 +01:00
import Data.Functor ( ( $> ) )
2021-07-04 18:42:24 +01:00
import Data.List ( find )
2021-07-25 20:23:52 +01:00
import Data.Maybe ( isJust , mapMaybe )
2021-07-04 18:42:24 +01:00
import Data.Text ( Text )
2021-06-25 18:18:24 +01:00
import qualified Data.Text as T
import Data.Text.Encoding ( encodeUtf8 )
2021-08-02 20:10:24 +01:00
import Numeric.Natural
2021-06-25 18:18:24 +01:00
import Simplex.Chat.Controller
2021-07-05 20:05:07 +01:00
import Simplex.Chat.Help
2021-07-07 22:46:38 +01:00
import Simplex.Chat.Input
2021-07-05 20:05:07 +01:00
import Simplex.Chat.Notification
2021-07-07 22:46:38 +01:00
import Simplex.Chat.Options ( ChatOpts ( .. ) )
2021-07-04 18:42:24 +01:00
import Simplex.Chat.Protocol
2021-07-05 20:05:07 +01:00
import Simplex.Chat.Store
2021-06-25 18:18:24 +01:00
import Simplex.Chat.Styled ( plain )
2021-07-05 20:05:07 +01:00
import Simplex.Chat.Terminal
2021-07-04 18:42:24 +01:00
import Simplex.Chat.Types
2021-07-05 20:05:07 +01:00
import Simplex.Chat.View
2021-06-25 18:18:24 +01:00
import Simplex.Messaging.Agent
2021-08-02 20:10:24 +01:00
import Simplex.Messaging.Agent.Env.SQLite ( AgentConfig ( .. ) , defaultAgentConfig )
2021-06-25 18:18:24 +01:00
import Simplex.Messaging.Agent.Protocol
2021-07-27 08:08:05 +01:00
import qualified Simplex.Messaging.Crypto as C
2021-06-25 18:18:24 +01:00
import Simplex.Messaging.Parsers ( parseAll )
2021-07-12 19:00:03 +01:00
import Simplex.Messaging.Util ( raceAny_ )
2021-07-27 08:08:05 +01:00
import System.Exit ( exitFailure , exitSuccess )
2021-07-05 19:54:44 +01:00
import System.IO ( hFlush , stdout )
import Text.Read ( readMaybe )
2021-07-07 22:46:38 +01:00
import UnliftIO.Async ( race_ )
2021-08-05 20:51:48 +01:00
import qualified UnliftIO.Exception as E
2021-06-25 18:18:24 +01:00
import UnliftIO.STM
data ChatCommand
= ChatHelp
| MarkdownHelp
2021-07-05 19:54:44 +01:00
| AddContact
| Connect SMPQueueInfo
2021-07-14 20:11:41 +01:00
| DeleteContact ContactName
| SendMessage ContactName ByteString
2021-07-12 19:00:03 +01:00
| NewGroup GroupProfile
2021-07-14 20:11:41 +01:00
| AddMember GroupName ContactName GroupMemberRole
2021-07-16 07:40:55 +01:00
| JoinGroup GroupName
2021-07-14 20:11:41 +01:00
| RemoveMember GroupName ContactName
| MemberRole GroupName ContactName GroupMemberRole
| LeaveGroup GroupName
| DeleteGroup GroupName
| ListMembers GroupName
| SendGroupMessage GroupName ByteString
2021-07-27 08:08:05 +01:00
| QuitChat
2021-06-25 18:18:24 +01:00
deriving ( Show )
2021-08-02 20:10:24 +01:00
data ChatConfig = ChatConfig
{ agentConfig :: AgentConfig ,
dbPoolSize :: Int ,
tbqSize :: Natural
}
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-08-02 20:10:24 +01:00
tbqSize = 16
2021-07-07 22:46:38 +01:00
}
logCfg :: LogConfig
logCfg = LogConfig { lc_file = Nothing , lc_stderr = True }
2021-08-02 20:10:24 +01:00
simplexChat :: WithTerminal t => ChatConfig -> ChatOpts -> t -> IO ()
simplexChat cfg opts t =
2021-07-07 22:46:38 +01:00
-- setLogLevel LogInfo -- LogError
-- withGlobalLogging logCfg $ do
initializeNotifications
2021-08-02 20:10:24 +01:00
>>= newChatController cfg opts t
2021-07-07 22:46:38 +01:00
>>= runSimplexChat
2021-08-02 20:10:24 +01:00
newChatController :: WithTerminal t => ChatConfig -> ChatOpts -> t -> ( Notification -> IO () ) -> IO ChatController
newChatController ChatConfig { agentConfig = cfg , dbPoolSize , tbqSize } ChatOpts { dbFile , smpServers } t sendNotification = do
chatStore <- createStore ( dbFile <> " .chat.db " ) dbPoolSize
2021-07-07 22:46:38 +01:00
currentUser <- getCreateActiveUser chatStore
chatTerminal <- newChatTerminal t
smpAgent <- getSMPAgentClient cfg { dbFile = dbFile <> " .agent.db " , smpServers }
2021-07-12 19:00:03 +01:00
idsDrg <- newTVarIO =<< drgNew
2021-08-02 20:10:24 +01:00
inputQ <- newTBQueueIO tbqSize
notifyQ <- newTBQueueIO tbqSize
2021-08-05 20:51:48 +01:00
chatLock <- newTMVarIO ()
pure ChatController { currentUser , smpAgent , chatTerminal , chatStore , idsDrg , inputQ , notifyQ , sendNotification , chatLock }
2021-07-07 22:46:38 +01:00
runSimplexChat :: ChatController -> IO ()
runSimplexChat = runReaderT ( race_ runTerminalInput runChatController )
2021-06-25 18:18:24 +01:00
runChatController :: ( MonadUnliftIO m , MonadReader ChatController m ) => m ()
2021-06-26 20:20:33 +01:00
runChatController =
raceAny_
[ inputSubscriber ,
agentSubscriber ,
notificationSubscriber
]
2021-06-25 18:18:24 +01:00
2021-08-05 20:51:48 +01:00
withLock :: MonadUnliftIO m => TMVar () -> m () -> m ()
withLock lock =
E . bracket_
( void . atomically $ takeTMVar lock )
( atomically $ putTMVar lock () )
2021-06-25 18:18:24 +01:00
inputSubscriber :: ( MonadUnliftIO m , MonadReader ChatController m ) => m ()
inputSubscriber = do
q <- asks inputQ
2021-08-05 20:51:48 +01:00
l <- asks chatLock
2021-06-25 18:18:24 +01:00
forever $
atomically ( readTBQueue q ) >>= \ case
InputControl _ -> pure ()
InputCommand s ->
case parseAll chatCommandP . encodeUtf8 $ T . pack s of
Left e -> printToView [ plain s , " invalid input: " <> plain e ]
Right cmd -> do
case cmd of
SendMessage c msg -> showSentMessage c msg
2021-07-24 10:26:28 +01:00
SendGroupMessage g msg -> showSentGroupMessage g msg
2021-06-25 18:18:24 +01:00
_ -> printToView [ plain s ]
2021-07-05 19:54:44 +01:00
user <- asks currentUser
2021-08-05 20:51:48 +01:00
withLock l . void . runExceptT $
processChatCommand user cmd ` catchError ` showChatError
2021-06-25 18:18:24 +01:00
2021-07-05 19:54:44 +01:00
processChatCommand :: ChatMonad m => User -> ChatCommand -> m ()
2021-07-12 19:00:03 +01:00
processChatCommand user @ User { userId , profile } = \ case
2021-06-25 18:18:24 +01:00
ChatHelp -> printToView chatHelpInfo
MarkdownHelp -> printToView markdownInfo
2021-07-05 19:54:44 +01:00
AddContact -> do
( connId , qInfo ) <- withAgent createConnection
withStore $ \ st -> createDirectConnection st userId connId
showInvitation qInfo
Connect qInfo -> do
2021-07-16 07:40:55 +01:00
connId <- withAgent $ \ a -> joinConnection a qInfo . directMessage $ XInfo profile
2021-07-05 19:54:44 +01:00
withStore $ \ st -> createDirectConnection st userId connId
2021-08-02 20:10:24 +01:00
DeleteContact cName ->
withStore ( \ st -> getContactGroupNames st userId cName ) >>= \ case
[] -> do
conns <- withStore $ \ st -> getContactConnections st userId cName
withAgent $ \ a -> forM_ conns $ \ Connection { agentConnId } ->
deleteConnection a agentConnId ` catchError ` \ ( _ :: AgentErrorType ) -> pure ()
withStore $ \ st -> deleteContact st userId cName
unsetActive $ ActiveC cName
showContactDeleted cName
gs -> showContactGroups cName gs
2021-07-16 07:40:55 +01:00
SendMessage cName msg -> do
contact <- withStore $ \ st -> getContact st userId cName
2021-07-24 10:26:28 +01:00
let msgEvent = XMsgNew $ MsgContent MTText [] [ MsgContentBody { contentType = SimplexContentType XCText , contentData = msg } ]
2021-07-16 07:40:55 +01:00
sendDirectMessage ( contactConnId contact ) msgEvent
setActive $ ActiveC cName
2021-07-12 19:00:03 +01:00
NewGroup gProfile -> do
gVar <- asks idsDrg
2021-07-16 07:40:55 +01:00
group <- withStore $ \ st -> createNewGroup st gVar user gProfile
showGroupCreated group
AddMember gName cName memRole -> do
( group , contact ) <- withStore $ \ st -> ( , ) <$> getGroup st user gName <*> getContact st userId cName
2021-07-12 19:00:03 +01:00
let Group { groupId , groupProfile , membership , members } = group
userRole = memberRole membership
userMemberId = memberId membership
2021-08-02 20:10:24 +01:00
when ( userRole < GRAdmin || userRole < memRole ) $ chatError CEGroupUserRole
when ( memberStatus membership == GSMemInvited ) $ chatError ( CEGroupNotJoined gName )
unless ( memberActive membership ) $ chatError CEGroupMemberNotActive
when ( isJust $ contactMember contact members ) $ chatError ( CEGroupDuplicateMember cName )
2021-07-12 19:00:03 +01:00
gVar <- asks idsDrg
( agentConnId , qInfo ) <- withAgent createConnection
2021-07-24 10:26:28 +01:00
GroupMember { memberId } <- withStore $ \ st -> createContactGroupMember st gVar user groupId contact memRole agentConnId
2021-07-16 07:40:55 +01:00
let msg = XGrpInv $ GroupInvitation ( userMemberId , userRole ) ( memberId , memRole ) qInfo groupProfile
sendDirectMessage ( contactConnId contact ) msg
2021-08-02 20:10:24 +01:00
showSentGroupInvitation gName cName
2021-07-24 10:26:28 +01:00
setActive $ ActiveG gName
2021-07-16 07:40:55 +01:00
JoinGroup gName -> do
2021-07-24 10:26:28 +01:00
ReceivedGroupInvitation { fromMember , userMember , queueInfo } <- withStore $ \ st -> getGroupInvitation st user gName
agentConnId <- withAgent $ \ a -> joinConnection a queueInfo . directMessage . XGrpAcpt $ memberId userMember
withStore $ \ st -> do
2021-08-02 20:10:24 +01:00
createMemberConnection st userId fromMember agentConnId
updateGroupMemberStatus st userId fromMember GSMemAccepted
updateGroupMemberStatus st userId userMember GSMemAccepted
2021-07-24 10:26:28 +01:00
MemberRole _gName _cName _mRole -> pure ()
2021-08-02 20:10:24 +01:00
RemoveMember gName cName -> do
Group { membership , members } <- withStore $ \ st -> getGroup st user gName
case find ( ( == cName ) . ( localDisplayName :: GroupMember -> ContactName ) ) members of
Nothing -> chatError $ CEGroupMemberNotFound cName
Just member -> do
let userRole = memberRole membership
when ( userRole < GRAdmin || userRole < memberRole member ) $ chatError CEGroupUserRole
sendGroupMessage members . XGrpMemDel $ memberId member
deleteMemberConnection member
withStore $ \ st -> updateGroupMemberStatus st userId member GSMemRemoved
showDeletedMember gName Nothing ( Just member )
LeaveGroup gName -> do
Group { membership , members } <- withStore $ \ st -> getGroup st user gName
sendGroupMessage members XGrpLeave
mapM_ deleteMemberConnection members
withStore $ \ st -> updateGroupMemberStatus st userId membership GSMemLeft
showLeftMemberUser gName
DeleteGroup gName -> do
g @ Group { membership , members } <- withStore $ \ st -> getGroup st user gName
let s = memberStatus membership
canDelete =
memberRole membership == GROwner
|| ( s == GSMemRemoved || s == GSMemLeft || s == GSMemGroupDeleted )
unless canDelete $ chatError CEGroupUserRole
when ( memberActive membership ) $ sendGroupMessage members XGrpDel
mapM_ deleteMemberConnection members
withStore $ \ st -> deleteGroup st user g
showGroupDeletedUser gName
2021-07-27 08:08:05 +01:00
ListMembers gName -> do
group <- withStore $ \ st -> getGroup st user gName
showGroupMembers group
2021-07-24 10:26:28 +01:00
SendGroupMessage gName msg -> do
-- TODO save sent messages
-- TODO save pending message delivery for members without connections
2021-08-02 20:10:24 +01:00
Group { members , membership } <- withStore $ \ st -> getGroup st user gName
unless ( memberActive membership ) $ chatError CEGroupMemberUserRemoved
2021-07-24 10:26:28 +01:00
let msgEvent = XMsgNew $ MsgContent MTText [] [ MsgContentBody { contentType = SimplexContentType XCText , contentData = msg } ]
sendGroupMessage members msgEvent
setActive $ ActiveG gName
2021-07-27 08:08:05 +01:00
QuitChat -> liftIO exitSuccess
2021-07-12 19:00:03 +01:00
where
2021-08-02 20:10:24 +01:00
contactMember :: Contact -> [ GroupMember ] -> Maybe GroupMember
contactMember Contact { contactId } =
find $ \ GroupMember { memberContactId = cId , memberStatus = s } ->
cId == Just contactId && s /= GSMemRemoved && s /= GSMemLeft
2021-06-25 18:18:24 +01:00
agentSubscriber :: ( MonadUnliftIO m , MonadReader ChatController m ) => m ()
agentSubscriber = do
2021-07-06 19:07:03 +01:00
q <- asks $ subQ . smpAgent
2021-08-05 20:51:48 +01:00
l <- asks chatLock
2021-07-25 20:23:52 +01:00
subscribeUserConnections
2021-06-25 18:18:24 +01:00
forever $ do
2021-07-06 19:07:03 +01:00
( _ , connId , msg ) <- atomically $ readTBQueue q
user <- asks currentUser
-- TODO handle errors properly
2021-08-05 20:51:48 +01:00
withLock l . void . runExceptT $
processAgentMessage user connId msg ` catchError ` ( liftIO . print )
2021-07-04 18:42:24 +01:00
2021-07-25 20:23:52 +01:00
subscribeUserConnections :: ( MonadUnliftIO m , MonadReader ChatController m ) => m ()
subscribeUserConnections = void . runExceptT $ do
user <- asks currentUser
subscribeContacts user
subscribeGroups user
where
subscribeContacts user = do
contacts <- withStore ( ` getUserContacts ` user )
forM_ contacts $ \ ct @ Contact { localDisplayName = c } ->
( subscribe ( contactConnId ct ) >> showContactSubscribed c ) ` catchError ` showContactSubError c
subscribeGroups user = do
2021-08-02 20:10:24 +01:00
groups <- withStore ( ` getUserGroups ` user )
forM_ groups $ \ Group { members , membership , localDisplayName = g } -> do
2021-07-25 20:23:52 +01:00
let connectedMembers = mapMaybe ( \ m -> ( m , ) <$> memberConnId m ) members
2021-08-02 20:10:24 +01:00
if null connectedMembers
then
if memberActive membership
then showGroupEmpty g
else showGroupRemoved g
else do
forM_ connectedMembers $ \ ( GroupMember { localDisplayName = c } , cId ) ->
subscribe cId ` catchError ` showMemberSubError g c
showGroupSubscribed g
2021-07-25 20:23:52 +01:00
subscribe cId = withAgent ( ` subscribeConnection ` cId )
2021-07-06 19:07:03 +01:00
processAgentMessage :: forall m . ChatMonad m => User -> ConnId -> ACommand 'Agent -> m ()
2021-07-16 07:40:55 +01:00
processAgentMessage user @ User { userId , profile } agentConnId agentMessage = do
chatDirection <- withStore $ \ st -> getConnectionChatDirection st user agentConnId
2021-07-24 18:11:04 +01:00
forM_ ( agentMsgConnStatus agentMessage ) $ \ status ->
withStore $ \ st -> updateConnectionStatus st ( fromConnection chatDirection ) status
2021-07-06 19:07:03 +01:00
case chatDirection of
2021-07-24 18:11:04 +01:00
ReceivedDirectMessage conn maybeContact ->
processDirectMessage agentMessage conn maybeContact
ReceivedGroupMessage conn gName m ->
processGroupMessage agentMessage conn gName m
where
isMember :: MemberId -> Group -> Bool
isMember memId Group { membership , members } =
memberId membership == memId || isJust ( find ( ( == memId ) . memberId ) members )
contactIsReady :: Contact -> Bool
contactIsReady Contact { activeConn } = connStatus activeConn == ConnReady
memberIsReady :: GroupMember -> Bool
memberIsReady GroupMember { activeConn } = maybe False ( ( == ConnReady ) . connStatus ) activeConn
agentMsgConnStatus :: ACommand 'Agent -> Maybe ConnStatus
agentMsgConnStatus = \ case
2021-08-05 08:38:39 +01:00
REQ _ _ -> Just ConnRequested
2021-07-24 18:11:04 +01:00
INFO _ -> Just ConnSndReady
CON -> Just ConnReady
_ -> Nothing
processDirectMessage :: ACommand 'Agent -> Connection -> Maybe Contact -> m ()
processDirectMessage agentMsg conn = \ case
Nothing -> case agentMsg of
2021-08-05 08:38:39 +01:00
REQ confId connInfo -> do
2021-07-24 18:11:04 +01:00
saveConnInfo conn connInfo
acceptAgentConnection conn confId $ XInfo profile
INFO connInfo ->
saveConnInfo conn connInfo
CON -> pure ()
_ -> messageError $ " unsupported agent event: " <> T . pack ( show agentMsg )
Just ct @ Contact { localDisplayName = c } -> case agentMsg of
2021-07-06 19:07:03 +01:00
MSG meta msgBody -> do
2021-07-11 12:22:22 +01:00
ChatMessage { chatMsgEvent } <- liftEither $ parseChatMessage msgBody
2021-07-04 18:42:24 +01:00
case chatMsgEvent of
2021-07-24 10:26:28 +01:00
XMsgNew ( MsgContent MTText [] body ) -> newTextMessage c meta $ find ( isSimplexContentType XCText ) body
2021-07-11 12:22:22 +01:00
XInfo _ -> pure () -- TODO profile update
2021-07-24 10:26:28 +01:00
XGrpInv gInv -> processGroupInvitation ct gInv
2021-07-27 08:08:05 +01:00
XInfoProbe probe -> xInfoProbe ct probe
XInfoProbeCheck probeHash -> xInfoProbeCheck ct probeHash
XInfoProbeOk probe -> xInfoProbeOk ct probe
2021-07-04 18:42:24 +01:00
_ -> pure ()
2021-08-05 08:38:39 +01:00
REQ confId connInfo -> do
2021-07-24 10:26:28 +01:00
-- confirming direct connection with a member
ChatMessage { chatMsgEvent } <- liftEither $ parseChatMessage connInfo
case chatMsgEvent of
XGrpMemInfo _memId _memProfile -> do
-- TODO check member ID
-- TODO update member profile
2021-07-24 18:11:04 +01:00
acceptAgentConnection conn confId XOk
2021-08-05 08:38:39 +01:00
_ -> messageError " REQ from member must have x.grp.mem.info "
2021-07-24 10:26:28 +01:00
INFO connInfo -> do
ChatMessage { chatMsgEvent } <- liftEither $ parseChatMessage connInfo
case chatMsgEvent of
XGrpMemInfo _memId _memProfile -> do
-- TODO check member ID
-- TODO update member profile
pure ()
XOk -> pure ()
_ -> messageError " INFO from member must have x.grp.mem.info or x.ok "
2021-07-24 18:11:04 +01:00
CON ->
2021-07-24 10:26:28 +01:00
withStore ( \ st -> getViaGroupMember st user ct ) >>= \ case
Nothing -> do
showContactConnected ct
setActive $ ActiveC c
showToast ( c <> " > " ) " connected "
Just ( gName , m ) ->
2021-07-27 08:08:05 +01:00
when ( memberIsReady m ) $ do
notifyMemberConnected gName m
when ( memberCategory m == GCPreMember ) $ probeMatchingContacts ct
2021-07-06 19:07:03 +01:00
END -> do
showContactDisconnected c
2021-07-24 10:26:28 +01:00
showToast ( c <> " > " ) " disconnected "
2021-07-06 19:07:03 +01:00
unsetActive $ ActiveC c
2021-07-24 18:11:04 +01:00
_ -> messageError $ " unexpected agent event: " <> T . pack ( show agentMsg )
2021-07-24 10:26:28 +01:00
2021-07-24 18:11:04 +01:00
processGroupMessage :: ACommand 'Agent -> Connection -> GroupName -> GroupMember -> m ()
processGroupMessage agentMsg conn gName m = case agentMsg of
2021-08-05 08:38:39 +01:00
REQ confId connInfo -> do
2021-07-24 18:11:04 +01:00
ChatMessage { chatMsgEvent } <- liftEither $ parseChatMessage connInfo
case memberCategory m of
GCInviteeMember ->
case chatMsgEvent of
XGrpAcpt memId
| memId == memberId m -> do
2021-08-02 20:10:24 +01:00
withStore $ \ st -> updateGroupMemberStatus st userId m GSMemAccepted
2021-07-24 18:11:04 +01:00
acceptAgentConnection conn confId XOk
| otherwise -> messageError " x.grp.acpt: memberId is different from expected "
2021-08-05 08:38:39 +01:00
_ -> messageError " REQ from invited member must have x.grp.acpt "
2021-07-24 18:11:04 +01:00
_ ->
case chatMsgEvent of
XGrpMemInfo memId _memProfile
| memId == memberId m -> do
-- TODO update member profile
Group { membership } <- withStore $ \ st -> getGroup st user gName
acceptAgentConnection conn confId $ XGrpMemInfo ( memberId membership ) profile
| otherwise -> messageError " x.grp.mem.info: memberId is different from expected "
2021-08-05 08:38:39 +01:00
_ -> messageError " REQ from member must have x.grp.mem.info "
2021-07-24 18:11:04 +01:00
INFO connInfo -> do
ChatMessage { chatMsgEvent } <- liftEither $ parseChatMessage connInfo
case chatMsgEvent of
XGrpMemInfo memId _memProfile
| memId == memberId m -> do
-- TODO update member profile
pure ()
| otherwise -> messageError " x.grp.mem.info: memberId is different from expected "
XOk -> pure ()
_ -> messageError " INFO from member must have x.grp.mem.info "
pure ()
CON -> do
group @ Group { members , membership } <- withStore $ \ st -> getGroup st user gName
withStore $ \ st -> do
2021-08-02 20:10:24 +01:00
updateGroupMemberStatus st userId m GSMemConnected
2021-07-27 08:08:05 +01:00
unless ( memberActive membership ) $
2021-08-02 20:10:24 +01:00
updateGroupMemberStatus st userId membership GSMemConnected
2021-07-24 18:11:04 +01:00
-- TODO forward any pending (GMIntroInvReceived) introductions
case memberCategory m of
GCHostMember -> do
showUserJoinedGroup gName
setActive $ ActiveG gName
showToast ( " # " <> gName ) " you are connected to group "
GCInviteeMember -> do
showJoinedGroupMember gName m
setActive $ ActiveG gName
showToast ( " # " <> gName ) $ " member " <> localDisplayName ( m :: GroupMember ) <> " is connected "
intros <- withStore $ \ st -> createIntroductions st group m
sendGroupMessage members . XGrpMemNew $ memberInfo m
forM_ intros $ \ intro -> do
sendDirectMessage agentConnId . XGrpMemIntro . memberInfo $ reMember intro
withStore $ \ st -> updateIntroStatus st intro GMIntroSent
_ -> do
-- TODO send probe and decide whether to use existing contact connection or the new contact connection
-- TODO notify member who forwarded introduction - question - where it is stored? There is via_contact but probably there should be via_member in group_members table
withStore ( \ st -> getViaGroupContact st user m ) >>= \ case
Nothing -> do
notifyMemberConnected gName m
messageError " implementation error: connected member does not have contact "
Just ct ->
2021-07-27 08:08:05 +01:00
when ( contactIsReady ct ) $ do
notifyMemberConnected gName m
when ( memberCategory m == GCPreMember ) $ probeMatchingContacts ct
2021-07-24 18:11:04 +01:00
MSG meta msgBody -> do
ChatMessage { chatMsgEvent } <- liftEither $ parseChatMessage msgBody
case chatMsgEvent of
XMsgNew ( MsgContent MTText [] body ) ->
newGroupTextMessage gName m meta $ find ( isSimplexContentType XCText ) body
XGrpMemNew memInfo -> xGrpMemNew gName m memInfo
XGrpMemIntro memInfo -> xGrpMemIntro gName m memInfo
XGrpMemInv memId introInv -> xGrpMemInv gName m memId introInv
XGrpMemFwd memInfo introInv -> xGrpMemFwd gName m memInfo introInv
2021-08-02 20:10:24 +01:00
XGrpMemDel memId -> xGrpMemDel gName m memId
XGrpLeave -> xGrpLeave gName m
XGrpDel -> xGrpDel gName m
2021-07-24 18:11:04 +01:00
_ -> messageError $ " unsupported message: " <> T . pack ( show chatMsgEvent )
_ -> messageError $ " unsupported agent event: " <> T . pack ( show agentMsg )
2021-07-24 10:26:28 +01:00
notifyMemberConnected :: GroupName -> GroupMember -> m ()
notifyMemberConnected gName m @ GroupMember { localDisplayName } = do
showConnectedToGroupMember gName m
setActive $ ActiveG gName
showToast ( " # " <> gName ) $ " member " <> localDisplayName <> " is connected "
2021-07-27 08:08:05 +01:00
probeMatchingContacts :: Contact -> m ()
probeMatchingContacts ct = do
gVar <- asks idsDrg
( probe , probeId ) <- withStore $ \ st -> createSentProbe st gVar userId ct
sendDirectMessage ( contactConnId ct ) $ XInfoProbe probe
cs <- withStore ( \ st -> getMatchingContacts st userId ct )
let probeHash = C . sha256Hash probe
forM_ cs $ \ c -> sendProbeHash c probeHash probeId ` catchError ` const ( pure () )
where
sendProbeHash c probeHash probeId = do
sendDirectMessage ( contactConnId c ) $ XInfoProbeCheck probeHash
withStore $ \ st -> createSentProbeHash st userId probeId c
2021-07-24 10:26:28 +01:00
messageWarning :: Text -> m ()
messageWarning = liftIO . print
messageError :: Text -> m ()
messageError = liftIO . print
newTextMessage :: ContactName -> MsgMeta -> Maybe MsgContentBody -> m ()
2021-07-07 22:46:38 +01:00
newTextMessage c meta = \ case
2021-07-24 10:26:28 +01:00
Just MsgContentBody { contentData = bs } -> do
2021-07-07 22:46:38 +01:00
let text = safeDecodeUtf8 bs
showReceivedMessage c ( snd $ broker meta ) text ( integrity meta )
2021-07-24 10:26:28 +01:00
showToast ( c <> " > " ) text
2021-07-07 22:46:38 +01:00
setActive $ ActiveC c
2021-07-24 10:26:28 +01:00
_ -> messageError " x.msg.new: no expected message body "
newGroupTextMessage :: GroupName -> GroupMember -> MsgMeta -> Maybe MsgContentBody -> m ()
newGroupTextMessage gName GroupMember { localDisplayName = c } meta = \ case
Just MsgContentBody { contentData = bs } -> do
let text = safeDecodeUtf8 bs
showReceivedGroupMessage gName c ( snd $ broker meta ) text ( integrity meta )
showToast ( " # " <> gName <> " " <> c <> " > " ) text
setActive $ ActiveG gName
_ -> messageError " x.msg.new: no expected message body "
2021-07-07 22:46:38 +01:00
2021-07-24 10:26:28 +01:00
processGroupInvitation :: Contact -> GroupInvitation -> m ()
processGroupInvitation ct @ Contact { localDisplayName } inv @ ( GroupInvitation ( fromMemId , fromRole ) ( memId , memRole ) _ _ ) = do
2021-08-02 20:10:24 +01:00
when ( fromRole < GRAdmin || fromRole < memRole ) $ chatError ( CEGroupContactRole localDisplayName )
when ( fromMemId == memId ) $ chatError CEGroupDuplicateMemberId
2021-07-16 07:40:55 +01:00
group <- withStore $ \ st -> createGroupInvitation st user ct inv
2021-07-24 10:26:28 +01:00
showReceivedGroupInvitation group localDisplayName memRole
2021-07-12 19:00:03 +01:00
2021-07-27 08:08:05 +01:00
xInfoProbe :: Contact -> ByteString -> m ()
xInfoProbe c2 probe = do
r <- withStore $ \ st -> matchReceivedProbe st userId c2 probe
forM_ r $ \ c1 -> probeMatch c1 c2 probe
xInfoProbeCheck :: Contact -> ByteString -> m ()
xInfoProbeCheck c1 probeHash = do
r <- withStore $ \ st -> matchReceivedProbeHash st userId c1 probeHash
2021-08-02 20:10:24 +01:00
forM_ r . uncurry $ probeMatch c1
2021-07-27 08:08:05 +01:00
probeMatch :: Contact -> Contact -> ByteString -> m ()
probeMatch c1 @ Contact { profile = p1 } c2 @ Contact { profile = p2 } probe =
when ( p1 == p2 ) $ do
sendDirectMessage ( contactConnId c1 ) $ XInfoProbeOk probe
mergeContacts c1 c2
xInfoProbeOk :: Contact -> ByteString -> m ()
xInfoProbeOk c1 probe = do
r <- withStore $ \ st -> matchSentProbe st userId c1 probe
forM_ r $ \ c2 -> mergeContacts c1 c2
mergeContacts :: Contact -> Contact -> m ()
mergeContacts to from = do
withStore $ \ st -> mergeContactRecords st userId to from
showContactsMerged to from
2021-07-06 19:07:03 +01:00
parseChatMessage :: ByteString -> Either ChatError ChatMessage
parseChatMessage msgBody = first ChatErrorMessage ( parseAll rawChatMessageP msgBody >>= toChatMessage )
saveConnInfo :: Connection -> ConnInfo -> m ()
saveConnInfo activeConn connInfo = do
2021-07-11 12:22:22 +01:00
ChatMessage { chatMsgEvent } <- liftEither $ parseChatMessage connInfo
2021-07-06 19:07:03 +01:00
case chatMsgEvent of
2021-07-11 12:22:22 +01:00
XInfo p ->
withStore $ \ st -> createDirectContact st userId activeConn p
2021-07-24 18:11:04 +01:00
-- TODO show/log error, other events in SMP confirmation
_ -> pure ()
xGrpMemNew :: GroupName -> GroupMember -> MemberInfo -> m ()
xGrpMemNew gName m memInfo @ ( MemberInfo memId _ _ ) = do
group @ Group { membership } <- withStore $ \ st -> getGroup st user gName
when ( memberId membership /= memId ) $
if isMember memId group
then messageError " x.grp.mem.new error: member already exists "
else do
newMember <- withStore $ \ st -> createNewGroupMember st user group memInfo GCPostMember GSMemAnnounced
showJoinedGroupMemberConnecting gName m newMember
xGrpMemIntro :: GroupName -> GroupMember -> MemberInfo -> m ()
xGrpMemIntro gName m memInfo @ ( MemberInfo memId _ _ ) =
case memberCategory m of
GCHostMember -> do
group <- withStore $ \ st -> getGroup st user gName
if isMember memId group
then messageWarning " x.grp.mem.intro ignored: member already exists "
else do
( groupConnId , groupQInfo ) <- withAgent createConnection
( directConnId , directQInfo ) <- withAgent createConnection
newMember <- withStore $ \ st -> createIntroReMember st user group m memInfo groupConnId directConnId
let msg = XGrpMemInv memId IntroInvitation { groupQInfo , directQInfo }
sendDirectMessage agentConnId msg
2021-08-02 20:10:24 +01:00
withStore $ \ st -> updateGroupMemberStatus st userId newMember GSMemIntroInvited
2021-07-24 18:11:04 +01:00
_ -> messageError " x.grp.mem.intro can be only sent by host member "
xGrpMemInv :: GroupName -> GroupMember -> MemberId -> IntroInvitation -> m ()
xGrpMemInv gName m memId introInv =
case memberCategory m of
GCInviteeMember -> do
group <- withStore $ \ st -> getGroup st user gName
case find ( ( == memId ) . memberId ) $ members group of
Nothing -> messageError " x.grp.mem.inv error: referenced member does not exists "
Just reMember -> do
intro <- withStore $ \ st -> saveIntroInvitation st reMember m introInv
case activeConn ( reMember :: GroupMember ) of
Nothing -> pure () -- this is not an error, introduction will be forwarded once the member is connected
Just Connection { agentConnId = reAgentConnId } -> do
sendDirectMessage reAgentConnId $ XGrpMemFwd ( memberInfo m ) introInv
withStore $ \ st -> updateIntroStatus st intro GMIntroInvForwarded
_ -> messageError " x.grp.mem.inv can be only sent by invitee member "
xGrpMemFwd :: GroupName -> GroupMember -> MemberInfo -> IntroInvitation -> m ()
xGrpMemFwd gName m memInfo @ ( MemberInfo memId _ _ ) introInv @ IntroInvitation { groupQInfo , directQInfo } = do
group @ Group { membership } <- withStore $ \ st -> getGroup st user gName
toMember <- case find ( ( == memId ) . memberId ) $ members group of
-- TODO if the missed messages are correctly sent as soon as there is connection before anything else is sent
-- the situation when member does not exist is an error
-- member receiving x.grp.mem.fwd should have also received x.grp.mem.new prior to that.
-- For now, this branch compensates for the lack of delayed message delivery.
Nothing -> withStore $ \ st -> createNewGroupMember st user group memInfo GCPostMember GSMemAnnounced
Just m' -> pure m'
withStore $ \ st -> saveMemberInvitation st toMember introInv
let msg = XGrpMemInfo ( memberId membership ) profile
groupConnId <- withAgent $ \ a -> joinConnection a groupQInfo $ directMessage msg
directConnId <- withAgent $ \ a -> joinConnection a directQInfo $ directMessage msg
withStore $ \ st -> createIntroToMemberContact st userId m toMember groupConnId directConnId
2021-07-06 19:07:03 +01:00
2021-08-02 20:10:24 +01:00
xGrpMemDel :: GroupName -> GroupMember -> MemberId -> m ()
xGrpMemDel gName m memId = do
Group { membership , members } <- withStore $ \ st -> getGroup st user gName
if memberId membership == memId
then do
mapM_ deleteMemberConnection members
withStore $ \ st -> updateGroupMemberStatus st userId membership GSMemRemoved
showDeletedMemberUser gName m
else case find ( ( == memId ) . memberId ) members of
Nothing -> messageError " x.grp.mem.del with unknown member ID "
Just member -> do
let mRole = memberRole m
if mRole < GRAdmin || mRole < memberRole member
then messageError " x.grp.mem.del with insufficient member permissions "
else do
deleteMemberConnection member
withStore $ \ st -> updateGroupMemberStatus st userId member GSMemRemoved
showDeletedMember gName ( Just m ) ( Just member )
xGrpLeave :: GroupName -> GroupMember -> m ()
xGrpLeave gName m = do
deleteMemberConnection m
withStore $ \ st -> updateGroupMemberStatus st userId m GSMemLeft
showLeftMember gName m
xGrpDel :: GroupName -> GroupMember -> m ()
xGrpDel gName m = do
when ( memberRole m /= GROwner ) $ chatError CEGroupUserRole
ms <- withStore $ \ st -> do
Group { members , membership } <- getGroup st user gName
updateGroupMemberStatus st userId membership GSMemGroupDeleted
pure members
mapM_ deleteMemberConnection ms
showGroupDeleted gName m
chatError :: ChatMonad m => ChatErrorType -> m ()
chatError = throwError . ChatError
deleteMemberConnection :: ChatMonad m => GroupMember -> m ()
deleteMemberConnection m = do
User { userId } <- asks currentUser
withAgent $ forM_ ( memberConnId m ) . deleteConnection
withStore $ \ st -> deleteGroupMemberConnection st userId m
2021-07-16 07:40:55 +01:00
sendDirectMessage :: ChatMonad m => ConnId -> ChatMsgEvent -> m ()
sendDirectMessage agentConnId chatMsgEvent =
void . withAgent $ \ a -> sendMessage a agentConnId $ directMessage chatMsgEvent
directMessage :: ChatMsgEvent -> ByteString
directMessage chatMsgEvent =
serializeRawChatMessage $
rawChatMessage ChatMessage { chatMsgId = Nothing , chatMsgEvent , chatDAG = Nothing }
2021-07-24 10:26:28 +01:00
sendGroupMessage :: ChatMonad m => [ GroupMember ] -> ChatMsgEvent -> m ()
sendGroupMessage members chatMsgEvent = do
let msg = directMessage chatMsgEvent
-- TODO once scheduled delivery is implemented memberActive should be changed to memberCurrent
withAgent $ \ a ->
forM_ ( filter memberActive members ) $
traverse ( \ connId -> sendMessage a connId msg ) . memberConnId
acceptAgentConnection :: ChatMonad m => Connection -> ConfirmationId -> ChatMsgEvent -> m ()
acceptAgentConnection conn @ Connection { agentConnId } confId msg = do
2021-08-05 08:38:39 +01:00
withAgent $ \ a -> acceptConnection a agentConnId confId $ directMessage msg
2021-07-24 10:26:28 +01:00
withStore $ \ st -> updateConnectionStatus st conn ConnAccepted
2021-07-05 19:54:44 +01:00
getCreateActiveUser :: SQLiteStore -> IO User
getCreateActiveUser st = do
user <-
getUsers st >>= \ case
[] -> newUser
users -> maybe ( selectUser users ) pure ( find activeUser users )
putStrLn $ " Current user: " <> userStr user
pure user
where
newUser :: IO User
newUser = do
putStrLn
" No user profiles found, it will be created now. \ n \
2021-07-14 20:11:41 +01:00
\ Please choose your display name and your full name .\ n \
2021-07-05 19:54:44 +01:00
\ They will be sent to your contacts when you connect .\ n \
\ They are only stored on your device and you can change them later . "
loop
where
loop = do
2021-07-14 20:11:41 +01:00
displayName <- getContactName
fullName <- T . pack <$> getWithPrompt " full name (optional) "
liftIO ( runExceptT $ createUser st Profile { displayName , fullName } True ) >>= \ case
Left SEDuplicateName -> do
putStrLn " chosen display name is already used by another profile on this device, choose another one "
2021-07-05 19:54:44 +01:00
loop
Left e -> putStrLn ( " database error " <> show e ) >> exitFailure
Right user -> pure user
selectUser :: [ User ] -> IO User
selectUser [ user ] = do
liftIO $ setActiveUser st ( userId user )
pure user
selectUser users = do
2021-07-05 20:05:07 +01:00
putStrLn " Select user profile: "
2021-07-05 19:54:44 +01:00
forM_ ( zip [ 1 .. ] users ) $ \ ( n :: Int , user ) -> putStrLn $ show n <> " - " <> userStr user
loop
where
loop = do
nStr <- getWithPrompt $ " user profile number (1 .. " <> show ( length users ) <> " ) "
case readMaybe nStr :: Maybe Int of
Nothing -> putStrLn " invalid user number " >> loop
Just n
| n <= 0 || n > length users -> putStrLn " invalid user number " >> loop
| otherwise -> do
let user = users !! ( n - 1 )
liftIO $ setActiveUser st ( userId user )
pure user
userStr :: User -> String
2021-07-14 20:11:41 +01:00
userStr User { localDisplayName , profile = Profile { fullName } } =
T . unpack $ localDisplayName <> if T . null fullName then " " else " ( " <> fullName <> " ) "
getContactName :: IO ContactName
getContactName = do
displayName <- getWithPrompt " display name (no spaces) "
if null displayName || isJust ( find ( == ' ' ) displayName )
then putStrLn " display name has space(s), choose another one " >> getContactName
else pure $ T . pack displayName
2021-07-05 19:54:44 +01:00
getWithPrompt :: String -> IO String
getWithPrompt s = putStr ( s <> " : " ) >> hFlush stdout >> getLine
2021-06-25 18:18:24 +01:00
2021-07-04 18:42:24 +01:00
showToast :: ( MonadUnliftIO m , MonadReader ChatController m ) => Text -> Text -> m ()
showToast title text = atomically . ( ` writeTBQueue ` Notification { title , text } ) =<< asks notifyQ
2021-06-26 20:20:33 +01:00
notificationSubscriber :: ( MonadUnliftIO m , MonadReader ChatController m ) => m ()
notificationSubscriber = do
ChatController { notifyQ , sendNotification } <- ask
forever $ atomically ( readTBQueue notifyQ ) >>= liftIO . sendNotification
2021-07-05 19:54:44 +01:00
withAgent :: ChatMonad m => ( AgentClient -> ExceptT AgentErrorType m a ) -> m a
withAgent action =
2021-06-25 18:18:24 +01:00
asks smpAgent
>>= runExceptT . action
2021-07-05 19:54:44 +01:00
>>= liftEither . first ChatErrorAgent
2021-06-25 18:18:24 +01:00
2021-07-04 18:42:24 +01:00
withStore ::
ChatMonad m =>
( forall m' . ( MonadUnliftIO m' , MonadError StoreError m' ) => SQLiteStore -> m' a ) ->
m a
2021-07-12 19:00:03 +01:00
withStore action =
asks chatStore
>>= runExceptT . action
>>= liftEither . first ChatErrorStore
2021-07-04 18:42:24 +01:00
2021-06-25 18:18:24 +01:00
chatCommandP :: Parser ChatCommand
chatCommandP =
( " /help " <|> " /h " ) $> ChatHelp
2021-07-24 10:26:28 +01:00
<|> ( " /group # " <|> " /group " <|> " /g # " <|> " /g " ) *> ( NewGroup <$> groupProfile )
<|> ( " /add # " <|> " /add " <|> " /a # " <|> " /a " ) *> ( AddMember <$> displayName <* A . space <*> displayName <*> memberRole )
<|> ( " /join # " <|> " /join " <|> " /j # " <|> " /j " ) *> ( JoinGroup <$> displayName )
2021-08-02 20:10:24 +01:00
<|> ( " /remove # " <|> " /remove " <|> " /rm # " <|> " /rm " ) *> ( RemoveMember <$> displayName <* A . space <*> displayName )
<|> ( " /leave # " <|> " /leave " <|> " /l # " <|> " /l " ) *> ( LeaveGroup <$> displayName )
2021-07-14 20:11:41 +01:00
<|> ( " /delete # " <|> " /d # " ) *> ( DeleteGroup <$> displayName )
2021-07-27 08:08:05 +01:00
<|> ( " /members # " <|> " /members " <|> " /ms # " <|> " /ms " ) *> ( ListMembers <$> displayName )
2021-07-14 20:11:41 +01:00
<|> A . char '#' *> ( SendGroupMessage <$> displayName <* A . space <*> A . takeByteString )
2021-07-05 19:54:44 +01:00
<|> ( " /connect " <|> " /c " ) *> ( Connect <$> smpQueueInfoP )
2021-08-02 20:10:24 +01:00
<|> ( " /connect " <|> " /c " ) $> AddContact
2021-07-14 20:11:41 +01:00
<|> ( " /delete @ " <|> " /delete " <|> " /d @ " <|> " /d " ) *> ( DeleteContact <$> displayName )
<|> A . char '@' *> ( SendMessage <$> displayName <*> ( A . space *> A . takeByteString ) )
2021-06-25 18:18:24 +01:00
<|> ( " /markdown " <|> " /m " ) $> MarkdownHelp
2021-07-27 08:08:05 +01:00
<|> ( " /quit " <|> " /q " ) $> QuitChat
2021-06-25 18:18:24 +01:00
where
2021-07-14 20:11:41 +01:00
displayName = safeDecodeUtf8 <$> ( B . cons <$> A . satisfy refChar <*> A . takeTill ( == ' ' ) )
2021-07-11 12:22:22 +01:00
refChar c = c > ' ' && c /= '#' && c /= '@'
2021-07-12 19:00:03 +01:00
groupProfile = do
2021-07-16 07:40:55 +01:00
gName <- displayName
fullName' <- safeDecodeUtf8 <$> ( A . space *> A . takeByteString ) <|> pure " "
pure GroupProfile { displayName = gName , fullName = if T . null fullName' then gName else fullName' }
2021-07-11 12:22:22 +01:00
memberRole =
2021-07-12 19:00:03 +01:00
( " owner " $> GROwner )
<|> ( " admin " $> GRAdmin )
<|> ( " normal " $> GRMember )
2021-07-24 10:26:28 +01:00
<|> pure GRAdmin