2021-06-25 18:18:24 +01:00
|
|
|
{-# LANGUAGE ConstraintKinds #-}
|
2022-01-28 10:41:09 +00:00
|
|
|
{-# LANGUAGE DataKinds #-}
|
2021-06-25 18:18:24 +01:00
|
|
|
{-# LANGUAGE DeriveAnyClass #-}
|
2022-01-26 21:20:08 +00:00
|
|
|
{-# LANGUAGE DeriveGeneric #-}
|
2021-07-05 19:54:44 +01:00
|
|
|
{-# LANGUAGE DuplicateRecordFields #-}
|
2021-06-25 18:18:24 +01:00
|
|
|
{-# LANGUAGE FlexibleContexts #-}
|
2021-07-05 19:54:44 +01:00
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
2021-06-25 18:18:24 +01:00
|
|
|
|
|
|
|
|
module Simplex.Chat.Controller where
|
|
|
|
|
|
|
|
|
|
import Control.Exception
|
|
|
|
|
import Control.Monad.Except
|
|
|
|
|
import Control.Monad.IO.Unlift
|
|
|
|
|
import Control.Monad.Reader
|
2021-07-12 19:00:03 +01:00
|
|
|
import Crypto.Random (ChaChaDRG)
|
2022-01-26 21:20:08 +00:00
|
|
|
import Data.Aeson (ToJSON)
|
|
|
|
|
import qualified Data.Aeson as J
|
2022-01-24 16:07:17 +00:00
|
|
|
import Data.ByteString.Char8 (ByteString)
|
2021-09-04 07:32:56 +01:00
|
|
|
import Data.Int (Int64)
|
|
|
|
|
import Data.Map.Strict (Map)
|
2022-01-24 16:07:17 +00:00
|
|
|
import Data.Text (Text)
|
2022-01-26 21:20:08 +00:00
|
|
|
import GHC.Generics (Generic)
|
2021-09-04 07:32:56 +01:00
|
|
|
import Numeric.Natural
|
2022-01-24 16:07:17 +00:00
|
|
|
import Simplex.Chat.Messages
|
2021-07-05 20:05:07 +01:00
|
|
|
import Simplex.Chat.Store (StoreError)
|
2021-07-04 18:42:24 +01:00
|
|
|
import Simplex.Chat.Types
|
2022-01-27 22:01:15 +00:00
|
|
|
import Simplex.Chat.Util (enumJSON, singleFieldJSON)
|
2021-06-25 18:18:24 +01:00
|
|
|
import Simplex.Messaging.Agent (AgentClient)
|
2021-09-04 07:32:56 +01:00
|
|
|
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig)
|
2022-01-24 16:07:17 +00:00
|
|
|
import Simplex.Messaging.Agent.Protocol
|
2021-07-04 18:42:24 +01:00
|
|
|
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore)
|
2022-01-27 22:01:15 +00:00
|
|
|
import Simplex.Messaging.Parsers (dropPrefix)
|
2022-01-24 16:07:17 +00:00
|
|
|
import Simplex.Messaging.Protocol (CorrId)
|
2021-09-04 07:32:56 +01:00
|
|
|
import System.IO (Handle)
|
2021-06-25 18:18:24 +01:00
|
|
|
import UnliftIO.STM
|
|
|
|
|
|
2021-11-07 21:57:05 +00:00
|
|
|
versionNumber :: String
|
2022-01-21 18:58:43 +00:00
|
|
|
versionNumber = "1.0.2"
|
2022-01-11 21:23:57 +00:00
|
|
|
|
|
|
|
|
versionStr :: String
|
|
|
|
|
versionStr = "SimpleX Chat v" <> versionNumber
|
|
|
|
|
|
|
|
|
|
updateStr :: String
|
|
|
|
|
updateStr = "To update run: curl -o- https://raw.githubusercontent.com/simplex-chat/simplex-chat/master/install.sh | bash"
|
2021-11-07 21:57:05 +00:00
|
|
|
|
2021-09-04 07:32:56 +01:00
|
|
|
data ChatConfig = ChatConfig
|
|
|
|
|
{ agentConfig :: AgentConfig,
|
|
|
|
|
dbPoolSize :: Int,
|
|
|
|
|
tbqSize :: Natural,
|
|
|
|
|
fileChunkSize :: Integer
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-21 11:09:33 +00:00
|
|
|
data ActiveTo = ActiveNone | ActiveC ContactName | ActiveG GroupName
|
|
|
|
|
deriving (Eq)
|
|
|
|
|
|
2021-06-25 18:18:24 +01:00
|
|
|
data ChatController = ChatController
|
2021-08-22 15:56:36 +01:00
|
|
|
{ currentUser :: TVar User,
|
2022-01-21 11:09:33 +00:00
|
|
|
activeTo :: TVar ActiveTo,
|
2021-12-13 12:05:57 +00:00
|
|
|
firstTime :: Bool,
|
2021-07-04 18:42:24 +01:00
|
|
|
smpAgent :: AgentClient,
|
|
|
|
|
chatStore :: SQLiteStore,
|
2021-07-12 19:00:03 +01:00
|
|
|
idsDrg :: TVar ChaChaDRG,
|
2022-01-24 16:07:17 +00:00
|
|
|
inputQ :: TBQueue String,
|
2022-01-26 21:20:08 +00:00
|
|
|
outputQ :: TBQueue (Maybe CorrId, ChatResponse),
|
2021-06-26 20:20:33 +01:00
|
|
|
notifyQ :: TBQueue Notification,
|
2021-08-05 20:51:48 +01:00
|
|
|
sendNotification :: Notification -> IO (),
|
2021-09-04 07:32:56 +01:00
|
|
|
chatLock :: TMVar (),
|
|
|
|
|
sndFiles :: TVar (Map Int64 Handle),
|
|
|
|
|
rcvFiles :: TVar (Map Int64 Handle),
|
|
|
|
|
config :: ChatConfig
|
2021-06-25 18:18:24 +01:00
|
|
|
}
|
|
|
|
|
|
2022-01-24 16:07:17 +00:00
|
|
|
data HelpSection = HSMain | HSFiles | HSGroups | HSMyAddress | HSMarkdown
|
2022-01-26 21:20:08 +00:00
|
|
|
deriving (Show, Generic)
|
|
|
|
|
|
|
|
|
|
instance ToJSON HelpSection where
|
2022-01-27 22:01:15 +00:00
|
|
|
toJSON = J.genericToJSON . enumJSON $ dropPrefix "HS"
|
|
|
|
|
toEncoding = J.genericToEncoding . enumJSON $ dropPrefix "HS"
|
2022-01-24 16:07:17 +00:00
|
|
|
|
|
|
|
|
data ChatCommand
|
2022-01-28 10:41:09 +00:00
|
|
|
= APIGetChats
|
|
|
|
|
| APIGetChat ChatType Int64
|
|
|
|
|
| APIGetChatItems Int
|
|
|
|
|
| ChatHelp HelpSection
|
2022-01-24 16:07:17 +00:00
|
|
|
| Welcome
|
|
|
|
|
| AddContact
|
|
|
|
|
| Connect (Maybe AConnectionRequestUri)
|
|
|
|
|
| ConnectAdmin
|
|
|
|
|
| DeleteContact ContactName
|
|
|
|
|
| ListContacts
|
|
|
|
|
| CreateMyAddress
|
|
|
|
|
| DeleteMyAddress
|
|
|
|
|
| ShowMyAddress
|
|
|
|
|
| AcceptContact ContactName
|
|
|
|
|
| RejectContact ContactName
|
|
|
|
|
| SendMessage ContactName ByteString
|
|
|
|
|
| NewGroup GroupProfile
|
|
|
|
|
| AddMember GroupName ContactName GroupMemberRole
|
|
|
|
|
| JoinGroup GroupName
|
|
|
|
|
| RemoveMember GroupName ContactName
|
|
|
|
|
| MemberRole GroupName ContactName GroupMemberRole
|
|
|
|
|
| LeaveGroup GroupName
|
|
|
|
|
| DeleteGroup GroupName
|
|
|
|
|
| ListMembers GroupName
|
|
|
|
|
| ListGroups
|
|
|
|
|
| SendGroupMessage GroupName ByteString
|
|
|
|
|
| SendFile ContactName FilePath
|
|
|
|
|
| SendGroupFile GroupName FilePath
|
2022-01-26 21:20:08 +00:00
|
|
|
| ReceiveFile FileTransferId (Maybe FilePath)
|
|
|
|
|
| CancelFile FileTransferId
|
|
|
|
|
| FileStatus FileTransferId
|
2022-01-24 16:07:17 +00:00
|
|
|
| ShowProfile
|
|
|
|
|
| UpdateProfile Profile
|
|
|
|
|
| QuitChat
|
|
|
|
|
| ShowVersion
|
|
|
|
|
deriving (Show)
|
|
|
|
|
|
|
|
|
|
data ChatResponse
|
2022-01-28 10:41:09 +00:00
|
|
|
= CRApiChats {chats :: [AChatPreview]}
|
|
|
|
|
| CRApiDirectChat {chat :: Chat 'CTDirect}
|
2022-01-29 16:06:08 +04:00
|
|
|
| CRApiGroupChat {gChat :: Chat 'CTGroup}
|
2022-01-28 10:41:09 +00:00
|
|
|
| CRNewChatItem {chatItem :: AChatItem}
|
2022-01-26 21:20:08 +00:00
|
|
|
| CRCmdAccepted {corr :: CorrId}
|
2022-01-27 22:01:15 +00:00
|
|
|
| CRChatHelp {helpSection :: HelpSection}
|
|
|
|
|
| CRWelcome {user :: User}
|
2022-01-26 21:20:08 +00:00
|
|
|
| CRGroupCreated {groupInfo :: GroupInfo}
|
|
|
|
|
| CRGroupMembers {group :: Group}
|
|
|
|
|
| CRContactsList {contacts :: [Contact]}
|
|
|
|
|
| CRUserContactLink {connReqContact :: ConnReqContact}
|
|
|
|
|
| CRContactRequestRejected {contactName :: ContactName} -- TODO
|
|
|
|
|
| CRUserAcceptedGroupSent {groupInfo :: GroupInfo}
|
|
|
|
|
| CRUserDeletedMember {groupInfo :: GroupInfo, member :: GroupMember}
|
|
|
|
|
| CRGroupsList {groups :: [GroupInfo]}
|
|
|
|
|
| CRSentGroupInvitation {groupInfo :: GroupInfo, contact :: Contact}
|
|
|
|
|
| CRFileTransferStatus (FileTransfer, [Integer]) -- TODO refactor this type to FileTransferStatus
|
|
|
|
|
| CRUserProfile {profile :: Profile}
|
2022-01-24 16:07:17 +00:00
|
|
|
| CRUserProfileNoChange
|
|
|
|
|
| CRVersionInfo
|
2022-01-26 21:20:08 +00:00
|
|
|
| CRInvitation {connReqInvitation :: ConnReqInvitation}
|
2022-01-24 16:07:17 +00:00
|
|
|
| CRSentConfirmation
|
|
|
|
|
| CRSentInvitation
|
|
|
|
|
| CRContactUpdated {fromContact :: Contact, toContact :: Contact}
|
|
|
|
|
| CRContactsMerged {intoContact :: Contact, mergedContact :: Contact}
|
2022-01-26 21:20:08 +00:00
|
|
|
| CRContactDeleted {contactName :: ContactName} -- TODO
|
|
|
|
|
| CRUserContactLinkCreated {connReqContact :: ConnReqContact}
|
2022-01-24 16:07:17 +00:00
|
|
|
| CRUserContactLinkDeleted
|
2022-01-26 21:20:08 +00:00
|
|
|
| CRReceivedContactRequest {contactName :: ContactName, profile :: Profile} -- TODO what is the entity here?
|
|
|
|
|
| CRAcceptingContactRequest {contactName :: ContactName} -- TODO
|
|
|
|
|
| CRLeftMemberUser {groupInfo :: GroupInfo}
|
|
|
|
|
| CRGroupDeletedUser {groupInfo :: GroupInfo}
|
|
|
|
|
| CRRcvFileAccepted {fileTransfer :: RcvFileTransfer, filePath :: FilePath}
|
|
|
|
|
| CRRcvFileAcceptedSndCancelled {rcvFileTransfer :: RcvFileTransfer}
|
|
|
|
|
| CRRcvFileStart {rcvFileTransfer :: RcvFileTransfer}
|
|
|
|
|
| CRRcvFileComplete {rcvFileTransfer :: RcvFileTransfer}
|
|
|
|
|
| CRRcvFileCancelled {rcvFileTransfer :: RcvFileTransfer}
|
|
|
|
|
| CRRcvFileSndCancelled {rcvFileTransfer :: RcvFileTransfer}
|
|
|
|
|
| CRSndFileStart {sndFileTransfer :: SndFileTransfer}
|
|
|
|
|
| CRSndFileComplete {sndFileTransfer :: SndFileTransfer}
|
|
|
|
|
| CRSndFileCancelled {sndFileTransfer :: SndFileTransfer}
|
|
|
|
|
| CRSndFileRcvCancelled {sndFileTransfer :: SndFileTransfer}
|
|
|
|
|
| CRSndGroupFileCancelled {sndFileTransfers :: [SndFileTransfer]}
|
2022-01-24 16:07:17 +00:00
|
|
|
| CRUserProfileUpdated {fromProfile :: Profile, toProfile :: Profile}
|
2022-01-26 21:20:08 +00:00
|
|
|
| CRContactConnected {contact :: Contact}
|
|
|
|
|
| CRContactAnotherClient {contact :: Contact}
|
|
|
|
|
| CRContactDisconnected {contact :: Contact}
|
|
|
|
|
| CRContactSubscribed {contact :: Contact}
|
|
|
|
|
| CRContactSubError {contact :: Contact, chatError :: ChatError}
|
|
|
|
|
| CRGroupInvitation {groupInfo :: GroupInfo}
|
|
|
|
|
| CRReceivedGroupInvitation {groupInfo :: GroupInfo, contact :: Contact, memberRole :: GroupMemberRole}
|
|
|
|
|
| CRUserJoinedGroup {groupInfo :: GroupInfo}
|
|
|
|
|
| CRJoinedGroupMember {groupInfo :: GroupInfo, member :: GroupMember}
|
|
|
|
|
| CRJoinedGroupMemberConnecting {groupInfo :: GroupInfo, hostMember :: GroupMember, member :: GroupMember}
|
|
|
|
|
| CRConnectedToGroupMember {groupInfo :: GroupInfo, member :: GroupMember}
|
|
|
|
|
| CRDeletedMember {groupInfo :: GroupInfo, byMember :: GroupMember, deletedMember :: GroupMember}
|
|
|
|
|
| CRDeletedMemberUser {groupInfo :: GroupInfo, member :: GroupMember}
|
|
|
|
|
| CRLeftMember {groupInfo :: GroupInfo, member :: GroupMember}
|
|
|
|
|
| CRGroupEmpty {groupInfo :: GroupInfo}
|
|
|
|
|
| CRGroupRemoved {groupInfo :: GroupInfo}
|
|
|
|
|
| CRGroupDeleted {groupInfo :: GroupInfo, member :: GroupMember}
|
|
|
|
|
| CRMemberSubError {groupInfo :: GroupInfo, contactName :: ContactName, chatError :: ChatError} -- TODO Contact? or GroupMember?
|
|
|
|
|
| CRGroupSubscribed {groupInfo :: GroupInfo}
|
|
|
|
|
| CRSndFileSubError {sndFileTransfer :: SndFileTransfer, chatError :: ChatError}
|
|
|
|
|
| CRRcvFileSubError {rcvFileTransfer :: RcvFileTransfer, chatError :: ChatError}
|
2022-01-24 16:07:17 +00:00
|
|
|
| CRUserContactLinkSubscribed
|
2022-01-26 21:20:08 +00:00
|
|
|
| CRUserContactLinkSubError {chatError :: ChatError}
|
|
|
|
|
| CRMessageError {severity :: Text, errorMessage :: Text}
|
|
|
|
|
| CRChatCmdError {chatError :: ChatError}
|
|
|
|
|
| CRChatError {chatError :: ChatError}
|
|
|
|
|
deriving (Show, Generic)
|
|
|
|
|
|
|
|
|
|
instance ToJSON ChatResponse where
|
2022-01-27 22:01:15 +00:00
|
|
|
toJSON = J.genericToJSON . singleFieldJSON $ dropPrefix "CR"
|
|
|
|
|
toEncoding = J.genericToEncoding . singleFieldJSON $ dropPrefix "CR"
|
2021-06-25 18:18:24 +01:00
|
|
|
|
2021-07-04 18:42:24 +01:00
|
|
|
data ChatError
|
2022-01-27 22:01:15 +00:00
|
|
|
= ChatError {errorType :: ChatErrorType}
|
|
|
|
|
| ChatErrorMessage {errorMessage :: String}
|
|
|
|
|
| ChatErrorAgent {agentError :: AgentErrorType}
|
|
|
|
|
| ChatErrorStore {storeError :: StoreError}
|
2022-01-28 10:41:09 +00:00
|
|
|
| ChatErrorNotImplemented
|
2022-01-26 21:20:08 +00:00
|
|
|
deriving (Show, Exception, Generic)
|
|
|
|
|
|
|
|
|
|
instance ToJSON ChatError where
|
2022-01-27 22:01:15 +00:00
|
|
|
toJSON = J.genericToJSON . singleFieldJSON $ dropPrefix "Chat"
|
|
|
|
|
toEncoding = J.genericToEncoding . singleFieldJSON $ dropPrefix "Chat"
|
2021-07-04 18:42:24 +01:00
|
|
|
|
2021-07-16 07:40:55 +01:00
|
|
|
data ChatErrorType
|
2021-07-24 10:26:28 +01:00
|
|
|
= CEGroupUserRole
|
2022-01-24 16:07:17 +00:00
|
|
|
| CEInvalidConnReq
|
2022-01-26 21:20:08 +00:00
|
|
|
| CEContactGroups {contactName :: ContactName, groupNames :: [GroupName]}
|
|
|
|
|
| CEGroupContactRole {contactName :: ContactName}
|
|
|
|
|
| CEGroupDuplicateMember {contactName :: ContactName}
|
2021-07-16 07:40:55 +01:00
|
|
|
| CEGroupDuplicateMemberId
|
2022-01-26 21:20:08 +00:00
|
|
|
| CEGroupNotJoined {groupInfo :: GroupInfo}
|
2021-07-24 10:26:28 +01:00
|
|
|
| CEGroupMemberNotActive
|
2021-08-02 20:10:24 +01:00
|
|
|
| CEGroupMemberUserRemoved
|
2022-01-26 21:20:08 +00:00
|
|
|
| CEGroupMemberNotFound {contactName :: ContactName}
|
|
|
|
|
| CEGroupMemberIntroNotFound {contactName :: ContactName}
|
|
|
|
|
| CEGroupCantResendInvitation {groupInfo :: GroupInfo, contactName :: ContactName}
|
|
|
|
|
| CEGroupInternal {message :: String}
|
|
|
|
|
| CEFileNotFound {message :: String}
|
|
|
|
|
| CEFileAlreadyReceiving {message :: String}
|
|
|
|
|
| CEFileAlreadyExists {filePath :: FilePath}
|
|
|
|
|
| CEFileRead {filePath :: FilePath, message :: String}
|
|
|
|
|
| CEFileWrite {filePath :: FilePath, message :: String}
|
|
|
|
|
| CEFileSend {fileId :: FileTransferId, agentError :: AgentErrorType}
|
|
|
|
|
| CEFileRcvChunk {message :: String}
|
|
|
|
|
| CEFileInternal {message :: String}
|
2022-01-11 08:50:44 +00:00
|
|
|
| CEAgentVersion
|
2022-01-26 21:20:08 +00:00
|
|
|
| CECommandError {message :: String}
|
|
|
|
|
deriving (Show, Exception, Generic)
|
|
|
|
|
|
|
|
|
|
instance ToJSON ChatErrorType where
|
2022-01-27 22:01:15 +00:00
|
|
|
toJSON = J.genericToJSON . singleFieldJSON $ dropPrefix "CE"
|
|
|
|
|
toEncoding = J.genericToEncoding . singleFieldJSON $ dropPrefix "CE"
|
2021-06-25 18:18:24 +01:00
|
|
|
|
2022-01-24 16:07:17 +00:00
|
|
|
type ChatMonad m = (MonadUnliftIO m, MonadReader ChatController m, MonadError ChatError m)
|
2021-06-25 18:18:24 +01:00
|
|
|
|
2021-07-04 18:42:24 +01:00
|
|
|
setActive :: (MonadUnliftIO m, MonadReader ChatController m) => ActiveTo -> m ()
|
2022-01-21 11:09:33 +00:00
|
|
|
setActive to = asks activeTo >>= atomically . (`writeTVar` to)
|
2021-06-25 18:18:24 +01:00
|
|
|
|
2021-07-04 18:42:24 +01:00
|
|
|
unsetActive :: (MonadUnliftIO m, MonadReader ChatController m) => ActiveTo -> m ()
|
2022-01-21 11:09:33 +00:00
|
|
|
unsetActive a = asks activeTo >>= atomically . (`modifyTVar` unset)
|
2021-06-25 18:18:24 +01:00
|
|
|
where
|
|
|
|
|
unset a' = if a == a' then ActiveNone else a'
|