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 )
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-07-07 22:46:38 +01:00
import Simplex.Messaging.Agent.Env.SQLite ( AgentConfig ( .. ) )
2021-06-25 18:18:24 +01:00
import Simplex.Messaging.Agent.Protocol
2021-07-07 22:46:38 +01:00
import Simplex.Messaging.Client ( smpDefaultConfig )
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-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-07-07 22:46:38 +01:00
cfg :: AgentConfig
cfg =
AgentConfig
{ tcpPort = undefined , -- agent does not listen to TCP
smpServers = undefined , -- filled in from options
rsaKeySize = 2048 ` div ` 8 ,
connIdBytes = 12 ,
tbqSize = 16 ,
dbFile = undefined , -- filled in from options
2021-07-24 10:26:28 +01:00
dbPoolSize = 1 ,
2021-07-07 22:46:38 +01:00
smpCfg = smpDefaultConfig
}
logCfg :: LogConfig
logCfg = LogConfig { lc_file = Nothing , lc_stderr = True }
simplexChat :: WithTerminal t => ChatOpts -> t -> IO ()
2021-07-24 10:26:28 +01:00
simplexChat opts t =
2021-07-07 22:46:38 +01:00
-- setLogLevel LogInfo -- LogError
-- withGlobalLogging logCfg $ do
initializeNotifications
>>= newChatController opts t
>>= runSimplexChat
newChatController :: WithTerminal t => ChatOpts -> t -> ( Notification -> IO () ) -> IO ChatController
newChatController ChatOpts { dbFile , smpServers } t sendNotification = do
2021-07-24 10:26:28 +01:00
chatStore <- createStore ( dbFile <> " .chat.db " ) 1
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-07-07 22:46:38 +01:00
inputQ <- newTBQueueIO $ tbqSize cfg
notifyQ <- newTBQueueIO $ tbqSize cfg
2021-07-12 19:00:03 +01:00
pure ChatController { currentUser , smpAgent , chatTerminal , chatStore , idsDrg , inputQ , notifyQ , sendNotification }
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
inputSubscriber :: ( MonadUnliftIO m , MonadReader ChatController m ) => m ()
inputSubscriber = do
q <- asks inputQ
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-07-06 19:07:03 +01:00
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-07-16 07:40:55 +01:00
DeleteContact cName -> do
conns <- withStore $ \ st -> getContactConnections st userId cName
2021-07-12 19:00:03 +01:00
withAgent $ \ a -> forM_ conns $ \ Connection { agentConnId } ->
deleteConnection a agentConnId ` catchError ` \ ( _ :: AgentErrorType ) -> pure ()
2021-07-16 07:40:55 +01:00
withStore $ \ st -> deleteContact st userId cName
unsetActive $ ActiveC cName
showContactDeleted cName
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-07-24 10:26:28 +01:00
when ( userRole < GRAdmin || userRole < memRole ) $ throwError $ ChatError CEGroupUserRole
2021-07-16 07:40:55 +01:00
when ( isMember contact members ) . throwError . ChatError $ CEGroupDuplicateMember cName
when ( memberStatus membership == GSMemInvited ) . throwError . ChatError $ CEGroupNotJoined gName
2021-07-24 10:26:28 +01:00
unless ( memberActive membership ) . throwError . ChatError $ CEGroupMemberNotActive
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
showSentGroupInvitation group 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
createMemberConnection st userId ( groupMemberId fromMember ) agentConnId
updateGroupMemberStatus st userId ( groupMemberId fromMember ) GSMemAccepted
updateGroupMemberStatus st userId ( groupMemberId userMember ) GSMemAccepted
MemberRole _gName _cName _mRole -> pure ()
RemoveMember _gName _cName -> pure ()
LeaveGroup _gName -> pure ()
DeleteGroup _gName -> pure ()
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
Group { members } <- withStore $ \ st -> getGroup st user gName
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-07-24 10:26:28 +01:00
isMember :: Contact -> [ GroupMember ] -> Bool
isMember Contact { contactId } members = isJust $ find ( ( == Just contactId ) . memberContactId ) members
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-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
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
groups <- filter ( not . null . members ) <$> withStore ( ` getUserGroups ` user )
forM_ groups $ \ Group { members , localDisplayName = g } -> do
let connectedMembers = mapMaybe ( \ m -> ( m , ) <$> memberConnId m ) members
forM_ connectedMembers $ \ ( GroupMember { localDisplayName = c } , cId ) ->
subscribe cId ` catchError ` showMemberSubError g c
showGroupSubscribed g
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
CONF _ _ -> Just ConnRequested
INFO _ -> Just ConnSndReady
CON -> Just ConnReady
_ -> Nothing
processDirectMessage :: ACommand 'Agent -> Connection -> Maybe Contact -> m ()
processDirectMessage agentMsg conn = \ case
Nothing -> case agentMsg of
CONF confId connInfo -> do
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-07-24 10:26:28 +01:00
CONF confId connInfo -> do
-- 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-07-24 10:26:28 +01:00
_ -> messageError " CONF from member must have x.grp.mem.info "
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
CONF confId connInfo -> do
ChatMessage { chatMsgEvent } <- liftEither $ parseChatMessage connInfo
case memberCategory m of
GCInviteeMember ->
case chatMsgEvent of
XGrpAcpt memId
| memId == memberId m -> do
withStore $ \ st -> updateGroupMemberStatus st userId ( groupMemberId m ) GSMemAccepted
acceptAgentConnection conn confId XOk
| otherwise -> messageError " x.grp.acpt: memberId is different from expected "
_ -> messageError " CONF from invited member must have x.grp.acpt "
_ ->
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 "
_ -> messageError " CONF from member must have x.grp.mem.info "
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
updateGroupMemberStatus st userId ( groupMemberId m ) GSMemConnected
2021-07-27 08:08:05 +01:00
unless ( memberActive membership ) $
updateGroupMemberStatus st userId ( groupMemberId 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
_ -> 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
when ( fromRole < GRAdmin || fromRole < memRole ) . throwError . ChatError $ CEGroupContactRole localDisplayName
2021-07-16 07:40:55 +01:00
when ( fromMemId == memId ) $ throwError $ ChatError CEGroupDuplicateMemberId
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
forM_ r $ \ ( c2 , probe ) -> probeMatch c1 c2 probe
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
withStore $ \ st -> updateGroupMemberStatus st userId ( groupMemberId newMember ) GSMemIntroInvited
_ -> 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-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
withAgent $ \ a -> allowConnection a agentConnId confId $ directMessage msg
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-07-14 20:11:41 +01:00
<|> ( " /remove # " <|> " /rm # " ) *> ( RemoveMember <$> displayName <* A . space <*> displayName )
<|> ( " /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
<|> ( " /add " <|> " /a " ) $> AddContact
<|> ( " /connect " <|> " /c " ) *> ( Connect <$> smpQueueInfoP )
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