diff --git a/cabal.project b/cabal.project index 39619484b6..9672e6c7ef 100644 --- a/cabal.project +++ b/cabal.project @@ -3,7 +3,7 @@ packages: . source-repository-package type: git location: git://github.com/simplex-chat/simplexmq.git - tag: b777a4fd93f888d549edf1877583fb7fc0e0196f + tag: 6fe3bfa980847c074b4cb0b9f3ea01cc5e6c567b source-repository-package type: git diff --git a/sha256map.nix b/sha256map.nix index 71494a7c5b..7134fe7564 100644 --- a/sha256map.nix +++ b/sha256map.nix @@ -1,5 +1,5 @@ { - "git://github.com/simplex-chat/simplexmq.git"."b777a4fd93f888d549edf1877583fb7fc0e0196f" = "0cnbc9swdzb29j3pv4z64w26sq8dsp4ixnnv5bbf5k6dz9bwl9zm"; + "git://github.com/simplex-chat/simplexmq.git"."6fe3bfa980847c074b4cb0b9f3ea01cc5e6c567b" = "0cnbc9swdzb29j3pv4z64w26sq8dsp4ixnnv5bbf5k6dz9bwl9zm"; "git://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "git://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; } diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index cab1767d05..8a25a5b584 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -52,7 +52,7 @@ import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (parseAll) -import Simplex.Messaging.Protocol (CorrId (..), MsgBody) +import Simplex.Messaging.Protocol (MsgBody) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Util (tryError) import System.Exit (exitFailure, exitSuccess) @@ -120,7 +120,7 @@ execChatCommand s = case parseAll chatCommandP . B.dropWhileEnd isSpace . encode toView :: ChatMonad m => ChatResponse -> m () toView event = do q <- asks outputQ - atomically $ writeTBQueue q (CorrId "", event) + atomically $ writeTBQueue q (Nothing, event) processChatCommand :: forall m. ChatMonad m => User -> ChatCommand -> m ChatResponse processChatCommand user@User {userId, profile} = \case @@ -136,7 +136,7 @@ processChatCommand user@User {userId, profile} = \case Connect (Just (ACR SCMContact cReq)) -> procCmd $ do connect cReq $ XContact profile Nothing pure CRSentInvitation - Connect Nothing -> chatError CEInvalidConnReq + Connect Nothing -> throwChatError CEInvalidConnReq ConnectAdmin -> procCmd $ do connect adminContactReq $ XContact profile Nothing pure CRSentInvitation @@ -145,12 +145,12 @@ processChatCommand user@User {userId, profile} = \case [] -> do conns <- withStore $ \st -> getContactConnections st userId cName procCmd $ do - withAgent $ \a -> forM_ conns $ \Connection {agentConnId} -> - deleteConnection a agentConnId `catchError` \(_ :: AgentErrorType) -> pure () + withAgent $ \a -> forM_ conns $ \conn -> + deleteConnection a (aConnId conn) `catchError` \(_ :: AgentErrorType) -> pure () withStore $ \st -> deleteContact st userId cName unsetActive $ ActiveC cName pure $ CRContactDeleted cName - gs -> chatError $ CEContactGroups cName gs + gs -> throwChatError $ CEContactGroups cName gs ListContacts -> CRContactsList <$> withStore (`getUserContacts` user) CreateMyAddress -> procCmd $ do (connId, cReq) <- withAgent (`createConnection` SCMContact) @@ -159,8 +159,8 @@ processChatCommand user@User {userId, profile} = \case DeleteMyAddress -> do conns <- withStore $ \st -> getUserContactLinkConnections st userId procCmd $ do - withAgent $ \a -> forM_ conns $ \Connection {agentConnId} -> - deleteConnection a agentConnId `catchError` \(_ :: AgentErrorType) -> pure () + withAgent $ \a -> forM_ conns $ \conn -> + deleteConnection a (aConnId conn) `catchError` \(_ :: AgentErrorType) -> pure () withStore $ \st -> deleteUserContactLink st userId pure CRUserContactLinkDeleted ShowMyAddress -> CRUserContactLink <$> withStore (`getUserContactLink` userId) @@ -191,9 +191,9 @@ processChatCommand user@User {userId, profile} = \case (group, contact) <- withStore $ \st -> (,) <$> getGroup st user gName <*> getContact st userId cName let Group gInfo@GroupInfo {groupId, groupProfile, membership} members = group GroupMember {memberRole = userRole, memberId = userMemberId} = membership - when (userRole < GRAdmin || userRole < memRole) $ chatError CEGroupUserRole - when (memberStatus membership == GSMemInvited) $ chatError (CEGroupNotJoined gInfo) - unless (memberActive membership) $ chatError CEGroupMemberNotActive + when (userRole < GRAdmin || userRole < memRole) $ throwChatError CEGroupUserRole + when (memberStatus membership == GSMemInvited) $ throwChatError (CEGroupNotJoined gInfo) + unless (memberActive membership) $ throwChatError CEGroupMemberNotActive let sendInvitation memberId cReq = do void . sendDirectMessage (contactConn contact) $ XGrpInv $ GroupInvitation (MemberIdRole userMemberId userRole) (MemberIdRole memberId memRole) cReq groupProfile @@ -209,8 +209,8 @@ processChatCommand user@User {userId, profile} = \case | memberStatus == GSMemInvited -> withStore (\st -> getMemberInvitation st user groupMemberId) >>= \case Just cReq -> sendInvitation memberId cReq - Nothing -> chatError $ CEGroupCantResendInvitation gInfo cName - | otherwise -> chatError $ CEGroupDuplicateMember cName + Nothing -> throwChatError $ CEGroupCantResendInvitation gInfo cName + | otherwise -> throwChatError $ CEGroupDuplicateMember cName JoinGroup gName -> do ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g} <- withStore $ \st -> getGroupInvitation st user gName procCmd $ do @@ -220,14 +220,14 @@ processChatCommand user@User {userId, profile} = \case updateGroupMemberStatus st userId fromMember GSMemAccepted updateGroupMemberStatus st userId (membership g) GSMemAccepted pure $ CRUserAcceptedGroupSent g - MemberRole _gName _cName _mRole -> chatError $ CECommandError "unsupported" + MemberRole _gName _cName _mRole -> throwChatError $ CECommandError "unsupported" RemoveMember gName cName -> do Group gInfo@GroupInfo {membership} members <- withStore $ \st -> getGroup st user gName case find ((== cName) . (localDisplayName :: GroupMember -> ContactName)) members of - Nothing -> chatError $ CEGroupMemberNotFound cName + Nothing -> throwChatError $ CEGroupMemberNotFound cName Just m@GroupMember {memberId = mId, memberRole = mRole, memberStatus = mStatus} -> do let userRole = memberRole (membership :: GroupMember) - when (userRole < GRAdmin || userRole < mRole) $ chatError CEGroupUserRole + when (userRole < GRAdmin || userRole < mRole) $ throwChatError CEGroupUserRole procCmd $ do when (mStatus /= GSMemInvited) . void . sendGroupMessage members $ XGrpMemDel mId deleteMemberConnection m @@ -246,7 +246,7 @@ processChatCommand user@User {userId, profile} = \case canDelete = memberRole (membership :: GroupMember) == GROwner || (s == GSMemRemoved || s == GSMemLeft || s == GSMemGroupDeleted || s == GSMemInvited) - unless canDelete $ chatError CEGroupUserRole + unless canDelete $ throwChatError CEGroupUserRole procCmd $ do when (memberActive membership) . void $ sendGroupMessage members XGrpDel mapM_ deleteMemberConnection members @@ -256,7 +256,7 @@ processChatCommand user@User {userId, profile} = \case ListGroups -> CRGroupsList <$> withStore (`getUserGroupDetails` user) SendGroupMessage gName msg -> do group@(Group gInfo@GroupInfo {membership} _) <- withStore $ \st -> getGroup st user gName - unless (memberActive membership) $ chatError CEGroupMemberUserRemoved + unless (memberActive membership) $ throwChatError CEGroupMemberUserRemoved let mc = MCText $ safeDecodeUtf8 msg ci <- sendGroupChatItem userId group (XMsgNew mc) (CIMsgContent mc) setActive $ ActiveG gName @@ -275,7 +275,7 @@ processChatCommand user@User {userId, profile} = \case SendGroupFile gName f -> do (fileSize, chSize) <- checkSndFile f Group gInfo@GroupInfo {membership} members <- withStore $ \st -> getGroup st user gName - unless (memberActive membership) $ chatError CEGroupMemberUserRemoved + unless (memberActive membership) $ throwChatError CEGroupMemberUserRemoved let fileName = takeFileName f ms <- forM (filter memberActive members) $ \m -> do (connId, fileConnReq) <- withAgent (`createConnection` SCMInvitation) @@ -287,12 +287,12 @@ processChatCommand user@User {userId, profile} = \case setActive $ ActiveG gName -- this is a hack as we have multiple direct messages instead of one per group let ciContent = CISndFileInvitation fileId f - ciMeta@CIMetaProps{itemId} <- saveChatItem userId (CDSndGroup gInfo) Nothing ciContent + ciMeta@CIMetaProps {itemId} <- saveChatItem userId (CDSndGroup gInfo) Nothing ciContent withStore $ \st -> updateFileTransferChatItemId st fileId itemId pure . CRNewChatItem $ AChatItem SCTGroup SMDSnd (GroupChat gInfo) $ SndGroupChatItem (CISndMeta ciMeta) ciContent ReceiveFile fileId filePath_ -> do ft@RcvFileTransfer {fileInvitation = FileInvitation {fileName, fileConnReq}, fileStatus} <- withStore $ \st -> getRcvFileTransfer st userId fileId - unless (fileStatus == RFSNew) . chatError $ CEFileAlreadyReceiving fileName + unless (fileStatus == RFSNew) . throwChatError $ CEFileAlreadyReceiving fileName procCmd $ do tryError (withAgent $ \a -> joinConnection a fileConnReq . directMessage $ XFileAcpt fileName) >>= \case Right agentConnId -> do @@ -336,8 +336,8 @@ processChatCommand user@User {userId, profile} = \case -- corrId <- liftIO $ CorrId <$> randomBytes gVar 8 -- q <- asks outputQ -- void . forkIO $ atomically . writeTBQueue q =<< - -- (corrId,) <$> (a `catchError` (pure . CRChatError)) - -- pure $ CRCommandAccepted corrId + -- (Just corrId,) <$> (a `catchError` (pure . CRChatError)) + -- pure $ CRCmdAccepted corrId -- a corrId connect :: ConnectionRequestUri c -> ChatMsgEvent -> m () connect cReq msg = do @@ -349,7 +349,7 @@ processChatCommand user@User {userId, profile} = \case cId == Just contactId && s /= GSMemRemoved && s /= GSMemLeft checkSndFile :: FilePath -> m (Integer, Integer) checkSndFile f = do - unlessM (doesFileExist f) . chatError $ CEFileNotFound f + unlessM (doesFileExist f) . throwChatError $ CEFileNotFound f (,) <$> getFileSize f <*> asks (fileChunkSize . config) getRcvFilePath :: Int64 -> Maybe FilePath -> String -> m FilePath getRcvFilePath fileId filePath fileName = case filePath of @@ -364,11 +364,11 @@ processChatCommand user@User {userId, profile} = \case (fPath `uniqueCombine` fileName >>= createEmptyFile) $ ifM (doesFileExist fPath) - (chatError $ CEFileAlreadyExists fPath) + (throwChatError $ CEFileAlreadyExists fPath) (createEmptyFile fPath) where createEmptyFile :: FilePath -> m FilePath - createEmptyFile fPath = emptyFile fPath `E.catch` (chatError . CEFileWrite fPath) + createEmptyFile fPath = emptyFile fPath `E.catch` (throwChatError . CEFileWrite fPath . (show :: E.SomeException -> String)) emptyFile :: FilePath -> m FilePath emptyFile fPath = do h <- getFileHandle fileId fPath rcvFiles AppendMode @@ -454,8 +454,7 @@ subscribeUserConnections = void . runExceptT $ do subscribe cId = withAgent (`subscribeConnection` cId) subscribeConns conns = withAgent $ \a -> - forM_ conns $ \Connection {agentConnId} -> - subscribeConnection a agentConnId + forM_ conns $ subscribeConnection a . aConnId processAgentMessage :: forall m. ChatMonad m => User -> ConnId -> ACommand 'Agent -> m () processAgentMessage user@User {userId, profile} agentConnId agentMessage = do @@ -685,7 +684,7 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = do cancelSndFileTransfer ft case err of SMP SMP.AUTH -> unless (fileStatus == FSCancelled) $ toView $ CRSndFileRcvCancelled ft - _ -> chatError $ CEFileSend fileId err + _ -> throwChatError $ CEFileSend fileId err MSG meta _ -> withAckMessage agentConnId meta $ pure () -- TODO print errors @@ -773,7 +772,7 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = do RFSCancelled _ -> pure () _ -> do cancelRcvFileTransfer ft - chatError $ CEFileRcvChunk err + throwChatError $ CEFileRcvChunk err notifyMemberConnected :: GroupInfo -> GroupMember -> m () notifyMemberConnected gInfo m@GroupMember {localDisplayName = c} = do @@ -841,8 +840,8 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = do processGroupInvitation :: Contact -> GroupInvitation -> m () processGroupInvitation ct@Contact {localDisplayName = c} inv@(GroupInvitation (MemberIdRole fromMemId fromRole) (MemberIdRole memId memRole) _ _) = do - when (fromRole < GRAdmin || fromRole < memRole) $ chatError (CEGroupContactRole c) - when (fromMemId == memId) $ chatError CEGroupDuplicateMemberId + when (fromRole < GRAdmin || fromRole < memRole) $ throwChatError (CEGroupContactRole c) + when (fromMemId == memId) $ throwChatError CEGroupDuplicateMemberId gInfo@GroupInfo {localDisplayName = gName} <- withStore $ \st -> createGroupInvitation st user ct inv toView $ CRReceivedGroupInvitation gInfo ct memRole showToast ("#" <> gName <> " " <> c <> "> ") "invited you to join the group" @@ -971,7 +970,7 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = do xGrpDel :: GroupInfo -> GroupMember -> m () xGrpDel gInfo m@GroupMember {memberRole} = do - when (memberRole /= GROwner) $ chatError CEGroupUserRole + when (memberRole /= GROwner) $ throwChatError CEGroupUserRole ms <- withStore $ \st -> do members <- getGroupMembers st user gInfo updateGroupMemberStatus st userId (membership gInfo) GSMemGroupDeleted @@ -1003,7 +1002,7 @@ sendFileChunkNo ft@SndFileTransfer {agentConnId = AgentConnId acId} chunkNo = do readFileChunk :: ChatMonad m => SndFileTransfer -> Integer -> m ByteString readFileChunk SndFileTransfer {fileId, filePath, chunkSize} chunkNo = - read_ `E.catch` (chatError . CEFileRead filePath) + read_ `E.catch` (throwChatError . CEFileRead filePath . (show :: E.SomeException -> String)) where read_ = do h <- getFileHandle fileId filePath sndFiles ReadMode @@ -1036,12 +1035,12 @@ appendFileChunk ft@RcvFileTransfer {fileId, fileStatus} chunkNo chunk = case fileStatus of RFSConnected RcvFileInfo {filePath} -> append_ filePath RFSCancelled _ -> pure () - _ -> chatError $ CEFileInternal "receiving file transfer not in progress" + _ -> throwChatError $ CEFileInternal "receiving file transfer not in progress" where append_ fPath = do h <- getFileHandle fileId fPath rcvFiles AppendMode E.try (liftIO $ B.hPut h chunk >> hFlush h) >>= \case - Left e -> chatError $ CEFileWrite fPath e + Left (e :: E.SomeException) -> throwChatError . CEFileWrite fPath $ show e Right () -> withStore $ \st -> updatedRcvFileChunkStored st ft chunkNo getFileHandle :: ChatMonad m => Int64 -> FilePath -> (ChatController -> TVar (Map Int64 Handle)) -> IOMode -> m Handle @@ -1088,8 +1087,8 @@ closeFileHandle fileId files = do h_ <- atomically . stateTVar fs $ \m -> (M.lookup fileId m, M.delete fileId m) mapM_ hClose h_ `E.catch` \(_ :: E.SomeException) -> pure () -chatError :: ChatMonad m => ChatErrorType -> m a -chatError = throwError . ChatError +throwChatError :: ChatMonad m => ChatErrorType -> m a +throwChatError = throwError . ChatError deleteMemberConnection :: ChatMonad m => GroupMember -> m () deleteMemberConnection m@GroupMember {activeConn} = do @@ -1115,8 +1114,8 @@ directMessage :: ChatMsgEvent -> ByteString directMessage chatMsgEvent = strEncode ChatMessage {chatMsgEvent} deliverMessage :: ChatMonad m => Connection -> MsgBody -> MessageId -> m () -deliverMessage Connection {connId, agentConnId} msgBody msgId = do - agentMsgId <- withAgent $ \a -> sendMessage a agentConnId msgBody +deliverMessage conn@Connection {connId} msgBody msgId = do + agentMsgId <- withAgent $ \a -> sendMessage a (aConnId conn) msgBody let sndMsgDelivery = SndMsgDelivery {connId, agentMsgId} withStore $ \st -> createSndMsgDelivery st sndMsgDelivery msgId @@ -1146,7 +1145,7 @@ sendPendingGroupMessages GroupMember {groupMemberId, localDisplayName} conn = do deliverMessage conn msgBody msgId withStore (\st -> deletePendingGroupMessage st groupMemberId msgId) when (cmEventTag == XGrpMemFwd_) $ case introId_ of - Nothing -> chatError $ CEGroupMemberIntroNotFound localDisplayName + Nothing -> throwChatError $ CEGroupMemberIntroNotFound localDisplayName Just introId -> withStore (\st -> updateIntroStatus st introId GMIntroInvForwarded) saveRcvMSG :: ChatMonad m => Connection -> MsgMeta -> MsgBody -> m (MessageId, ChatMsgEvent) @@ -1212,8 +1211,8 @@ mkCIMetaProps itemId itemTs createdAt = do pure CIMetaProps {itemId, itemTs, localItemTs, createdAt} allowAgentConnection :: ChatMonad m => Connection -> ConfirmationId -> ChatMsgEvent -> m () -allowAgentConnection conn@Connection {agentConnId} confId msg = do - withAgent $ \a -> allowConnection a agentConnId confId $ directMessage msg +allowAgentConnection conn confId msg = do + withAgent $ \a -> allowConnection a (aConnId conn) confId $ directMessage msg withStore $ \st -> updateConnectionStatus st conn ConnAccepted getCreateActiveUser :: SQLiteStore -> IO User diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index afff5ae9aa..dff3e67ab8 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -1,5 +1,6 @@ {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-} @@ -11,10 +12,13 @@ import Control.Monad.Except import Control.Monad.IO.Unlift import Control.Monad.Reader import Crypto.Random (ChaChaDRG) +import Data.Aeson (ToJSON) +import qualified Data.Aeson as J import Data.ByteString.Char8 (ByteString) import Data.Int (Int64) import Data.Map.Strict (Map) import Data.Text (Text) +import GHC.Generics (Generic) import Numeric.Natural import Simplex.Chat.Messages import Simplex.Chat.Store (StoreError) @@ -23,6 +27,7 @@ import Simplex.Messaging.Agent (AgentClient) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig) import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore) +import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) import Simplex.Messaging.Protocol (CorrId) import System.IO (Handle) import UnliftIO.STM @@ -54,7 +59,7 @@ data ChatController = ChatController chatStore :: SQLiteStore, idsDrg :: TVar ChaChaDRG, inputQ :: TBQueue String, - outputQ :: TBQueue (CorrId, ChatResponse), + outputQ :: TBQueue (Maybe CorrId, ChatResponse), notifyQ :: TBQueue Notification, sendNotification :: Notification -> IO (), chatLock :: TMVar (), @@ -64,7 +69,11 @@ data ChatController = ChatController } data HelpSection = HSMain | HSFiles | HSGroups | HSMyAddress | HSMarkdown - deriving (Show) + deriving (Show, Generic) + +instance ToJSON HelpSection where + toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "HS" + toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "HS" data ChatCommand = ChatHelp HelpSection @@ -92,9 +101,9 @@ data ChatCommand | SendGroupMessage GroupName ByteString | SendFile ContactName FilePath | SendGroupFile GroupName FilePath - | ReceiveFile Int64 (Maybe FilePath) - | CancelFile Int64 - | FileStatus Int64 + | ReceiveFile FileTransferId (Maybe FilePath) + | CancelFile FileTransferId + | FileStatus FileTransferId | ShowProfile | UpdateProfile Profile | QuitChat @@ -102,107 +111,119 @@ data ChatCommand deriving (Show) data ChatResponse - = CRNewChatItem AChatItem - | CRCommandAccepted CorrId + = CRNewChatItem {chatItem :: AChatItem} + | CRCmdAccepted {corr :: CorrId} | CRChatHelp HelpSection | CRWelcome User - | CRGroupCreated GroupInfo - | CRGroupMembers Group - | CRContactsList [Contact] - | CRUserContactLink ConnReqContact - | CRContactRequestRejected ContactName -- TODO - | CRUserAcceptedGroupSent GroupInfo - | CRUserDeletedMember GroupInfo GroupMember - | CRGroupsList [GroupInfo] - | CRSentGroupInvitation GroupInfo Contact - | CRFileTransferStatus (FileTransfer, [Integer]) - | CRUserProfile Profile + | 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} | CRUserProfileNoChange | CRVersionInfo - | CRInvitation ConnReqInvitation + | CRInvitation {connReqInvitation :: ConnReqInvitation} | CRSentConfirmation | CRSentInvitation | CRContactUpdated {fromContact :: Contact, toContact :: Contact} | CRContactsMerged {intoContact :: Contact, mergedContact :: Contact} - | CRContactDeleted ContactName -- TODO - | CRUserContactLinkCreated ConnReqContact + | CRContactDeleted {contactName :: ContactName} -- TODO + | CRUserContactLinkCreated {connReqContact :: ConnReqContact} | CRUserContactLinkDeleted - | CRReceivedContactRequest ContactName Profile -- TODO what is the entity here? - | CRAcceptingContactRequest ContactName -- TODO - | CRLeftMemberUser GroupInfo - | CRGroupDeletedUser GroupInfo - | CRRcvFileAccepted RcvFileTransfer FilePath - | CRRcvFileAcceptedSndCancelled RcvFileTransfer - | CRRcvFileStart RcvFileTransfer - | CRRcvFileComplete RcvFileTransfer - | CRRcvFileCancelled RcvFileTransfer - | CRRcvFileSndCancelled RcvFileTransfer - | CRSndFileStart SndFileTransfer - | CRSndFileComplete SndFileTransfer - | CRSndFileCancelled SndFileTransfer - | CRSndFileRcvCancelled SndFileTransfer - | CRSndGroupFileCancelled [SndFileTransfer] + | 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]} | CRUserProfileUpdated {fromProfile :: Profile, toProfile :: Profile} - | CRContactConnected Contact - | CRContactAnotherClient Contact - | CRContactDisconnected Contact - | CRContactSubscribed Contact - | CRContactSubError Contact ChatError - | CRGroupInvitation GroupInfo - | CRReceivedGroupInvitation GroupInfo Contact GroupMemberRole - | CRUserJoinedGroup GroupInfo - | CRJoinedGroupMember GroupInfo GroupMember - | CRJoinedGroupMemberConnecting {group :: GroupInfo, hostMember :: GroupMember, member :: GroupMember} - | CRConnectedToGroupMember GroupInfo GroupMember - | CRDeletedMember {group :: GroupInfo, byMember :: GroupMember, deletedMember :: GroupMember} - | CRDeletedMemberUser GroupInfo GroupMember - | CRLeftMember GroupInfo GroupMember - | CRGroupEmpty GroupInfo - | CRGroupRemoved GroupInfo - | CRGroupDeleted GroupInfo GroupMember - | CRMemberSubError GroupInfo ContactName ChatError -- TODO Contact? or GroupMember? - | CRGroupSubscribed GroupInfo - | CRSndFileSubError SndFileTransfer ChatError - | CRRcvFileSubError RcvFileTransfer ChatError + | 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} | CRUserContactLinkSubscribed - | CRUserContactLinkSubError ChatError - | CRMessageError Text Text - | CRChatCmdError ChatError - | CRChatError ChatError - deriving (Show) + | CRUserContactLinkSubError {chatError :: ChatError} + | CRMessageError {severity :: Text, errorMessage :: Text} + | CRChatCmdError {chatError :: ChatError} + | CRChatError {chatError :: ChatError} + deriving (Show, Generic) + +instance ToJSON ChatResponse where + toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "CR" + toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "CR" data ChatError = ChatError ChatErrorType | ChatErrorMessage String | ChatErrorAgent AgentErrorType | ChatErrorStore StoreError - deriving (Show, Exception) + deriving (Show, Exception, Generic) + +instance ToJSON ChatError where + toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "Chat" + toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "Chat" data ChatErrorType = CEGroupUserRole | CEInvalidConnReq - | CEContactGroups ContactName [GroupName] - | CEGroupContactRole ContactName - | CEGroupDuplicateMember ContactName + | CEContactGroups {contactName :: ContactName, groupNames :: [GroupName]} + | CEGroupContactRole {contactName :: ContactName} + | CEGroupDuplicateMember {contactName :: ContactName} | CEGroupDuplicateMemberId - | CEGroupNotJoined GroupInfo + | CEGroupNotJoined {groupInfo :: GroupInfo} | CEGroupMemberNotActive | CEGroupMemberUserRemoved - | CEGroupMemberNotFound ContactName - | CEGroupMemberIntroNotFound ContactName - | CEGroupCantResendInvitation GroupInfo ContactName - | CEGroupInternal String - | CEFileNotFound String - | CEFileAlreadyReceiving String - | CEFileAlreadyExists FilePath - | CEFileRead FilePath SomeException - | CEFileWrite FilePath SomeException - | CEFileSend Int64 AgentErrorType - | CEFileRcvChunk String - | CEFileInternal String + | 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} | CEAgentVersion - | CECommandError String - deriving (Show, Exception) + | CECommandError {message :: String} + deriving (Show, Exception, Generic) + +instance ToJSON ChatErrorType where + toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "CE" + toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "CE" type ChatMonad m = (MonadUnliftIO m, MonadReader ChatController m, MonadError ChatError m) diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index 2f86c8c2ed..0c0a45c0f2 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -13,7 +13,7 @@ module Simplex.Chat.Messages where -import Data.Aeson (FromJSON, ToJSON, (.=)) +import Data.Aeson (FromJSON, ToJSON) import qualified Data.Aeson as J import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Lazy.Char8 as LB @@ -27,11 +27,13 @@ import Data.Type.Equality import Data.Typeable (Typeable) import Database.SQLite.Simple.FromField (FromField (..)) import Database.SQLite.Simple.ToField (ToField (..)) -import GHC.Generics +import GHC.Generics (Generic) import Simplex.Chat.Protocol import Simplex.Chat.Types -import Simplex.Messaging.Agent.Protocol (AgentMsgId, MsgIntegrity, MsgMeta (..), serializeMsgIntegrity) +import Simplex.Messaging.Agent.Protocol (AgentMsgId, MsgIntegrity, MsgMeta (..)) import Simplex.Messaging.Agent.Store.SQLite (fromTextField_) +import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) import Simplex.Messaging.Protocol (MsgBody) data ChatType = CTDirect | CTGroup @@ -43,6 +45,24 @@ data ChatInfo (c :: ChatType) where deriving instance Show (ChatInfo c) +data JSONChatInfo + = JCInfoDirect {contact :: Contact} + | JCInfoGroup {groupInfo :: GroupInfo} + deriving (Generic) + +instance ToJSON JSONChatInfo where + toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "JCInfo" + toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "JCInfo" + +instance ToJSON (ChatInfo c) where + toJSON = J.toJSON . jsonChatInfo + toEncoding = J.toEncoding . jsonChatInfo + +jsonChatInfo :: ChatInfo c -> JSONChatInfo +jsonChatInfo = \case + DirectChat c -> JCInfoDirect c + GroupChat g -> JCInfoGroup g + type ChatItemData d = (CIMeta d, CIContent d) data ChatItem (c :: ChatType) (d :: MsgDirection) where @@ -52,6 +72,26 @@ data ChatItem (c :: ChatType) (d :: MsgDirection) where deriving instance Show (ChatItem c d) +data JSONChatItem d + = JCItemDirect {meta :: CIMeta d, content :: CIContent d} + | JCItemSndGroup {meta :: CIMeta d, content :: CIContent d} + | JCItemRcvGroup {member :: GroupMember, meta :: CIMeta d, content :: CIContent d} + deriving (Generic) + +instance ToJSON (JSONChatItem d) where + toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "JCItem" + toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "JCItem" + +instance ToJSON (ChatItem c d) where + toJSON = J.toJSON . jsonChatItem + toEncoding = J.toEncoding . jsonChatItem + +jsonChatItem :: ChatItem c d -> JSONChatItem d +jsonChatItem = \case + DirectChatItem meta cic -> JCItemDirect meta cic + SndGroupChatItem meta cic -> JCItemSndGroup meta cic + RcvGroupChatItem m meta cic -> JCItemRcvGroup m meta cic + data CChatItem c = forall d. CChatItem (SMsgDirection d) (ChatItem c d) deriving instance Show (CChatItem c) @@ -92,19 +132,50 @@ data AChatItem = forall c d. AChatItem (SChatType c) (SMsgDirection d) (ChatInfo deriving instance Show AChatItem +instance ToJSON AChatItem where + toJSON (AChatItem _ _ chat item) = J.toJSON $ JSONAnyChatItem chat item + toEncoding (AChatItem _ _ chat item) = J.toEncoding $ JSONAnyChatItem chat item + +data JSONAnyChatItem c d = JSONAnyChatItem {chatInfo :: ChatInfo c, chatItem :: ChatItem c d} + deriving (Generic) + +instance ToJSON (JSONAnyChatItem c d) where + toJSON = J.genericToJSON J.defaultOptions + toEncoding = J.genericToEncoding J.defaultOptions + data CIMeta (d :: MsgDirection) where CISndMeta :: CIMetaProps -> CIMeta 'MDSnd CIRcvMeta :: CIMetaProps -> MsgIntegrity -> CIMeta 'MDRcv deriving instance Show (CIMeta d) +instance ToJSON (CIMeta d) where + toJSON = J.toJSON . jsonCIMeta + toEncoding = J.toEncoding . jsonCIMeta + +data JSONCIMeta + = JCIMetaSnd {meta :: CIMetaProps} + | JCIMetaRcv {meta :: CIMetaProps, integrity :: MsgIntegrity} + deriving (Generic) + +instance ToJSON JSONCIMeta where + toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "JCIMeta" + toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "JCIMeta" + +jsonCIMeta :: CIMeta d -> JSONCIMeta +jsonCIMeta = \case + CISndMeta meta -> JCIMetaSnd meta + CIRcvMeta meta integrity -> JCIMetaRcv meta integrity + data CIMetaProps = CIMetaProps { itemId :: ChatItemId, itemTs :: ChatItemTs, localItemTs :: ZonedTime, createdAt :: UTCTime } - deriving (Show) + deriving (Show, Generic, FromJSON) + +instance ToJSON CIMetaProps where toEncoding = J.genericToEncoding J.defaultOptions type ChatItemId = Int64 @@ -120,26 +191,24 @@ deriving instance Show (CIContent d) instance ToField (CIContent d) where toField = toField . decodeLatin1 . LB.toStrict . J.encode instance ToJSON (CIContent d) where - toJSON = J.toJSON . ciContentToJSON - toEncoding = J.toEncoding . ciContentToJSON + toJSON = J.toJSON . jsonCIContent + toEncoding = J.toEncoding . jsonCIContent -data CIContentJSON = CIContentJSON - { tag :: Text, - subTag :: Maybe Text, - args :: J.Value - } - deriving (Generic, FromJSON) +data JSONCIContent + = JCIMsgContent {msgContent :: MsgContent} + | JCISndFileInvitation {fileId :: FileTransferId, filePath :: FilePath} + | JCIRcvFileInvitation {rcvFileTransfer :: RcvFileTransfer} + deriving (Generic) -instance ToJSON CIContentJSON where toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} +instance ToJSON JSONCIContent where + toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "JCI" + toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "JCI" -ciContentToJSON :: CIContent d -> CIContentJSON -ciContentToJSON = \case - CIMsgContent mc -> o "content" "" $ J.object ["content" .= mc] - CISndFileInvitation fId fPath -> o "sndFile" "invitation" $ J.object ["fileId" .= fId, "filePath" .= fPath] - CIRcvFileInvitation ft -> o "rcvFile" "invitation" $ J.object ["fileTransfer" .= ft] - where - o tag "" args = CIContentJSON {tag, subTag = Nothing, args} - o tag st args = CIContentJSON {tag, subTag = Just st, args} +jsonCIContent :: CIContent d -> JSONCIContent +jsonCIContent = \case + CIMsgContent mc -> JCIMsgContent mc + CISndFileInvitation fId fPath -> JCISndFileInvitation fId fPath + CIRcvFileInvitation ft -> JCIRcvFileInvitation ft ciContentToText :: CIContent d -> Text ciContentToText = \case @@ -241,7 +310,7 @@ instance ToJSON MsgMetaJSON where toEncoding = J.genericToEncoding J.defaultOpti msgMetaToJson :: MsgMeta -> MsgMetaJSON msgMetaToJson MsgMeta {integrity, recipient = (rcvId, rcvTs), broker = (serverId, serverTs), sndMsgId = sndId} = MsgMetaJSON - { integrity = (decodeLatin1 . serializeMsgIntegrity) integrity, + { integrity = (decodeLatin1 . strEncode) integrity, rcvId, rcvTs, serverId = (decodeLatin1 . B64.encode) serverId, diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index 0f9940bcc0..cdcf40cdd3 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -13,19 +13,17 @@ import Control.Monad.Reader import Data.Aeson (ToJSON (..), (.=)) import qualified Data.Aeson as J import qualified Data.Aeson.Encoding as JE -import qualified Data.ByteString.Base64.URL as U import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as LB import Data.List (find) import Foreign.C.String import Foreign.StablePtr -import GHC.Generics +import GHC.Generics (Generic) import Simplex.Chat import Simplex.Chat.Controller import Simplex.Chat.Options import Simplex.Chat.Store import Simplex.Chat.Types -import Simplex.Chat.View import Simplex.Messaging.Protocol (CorrId (..)) foreign export ccall "chat_init_store" cChatInitStore :: CString -> IO (StablePtr ChatStore) @@ -97,19 +95,19 @@ getActiveUser_ st = find activeUser <$> getUsers st -- | returns JSON in the form `{"user": }` or `{}` chatGetUser :: ChatStore -> IO JSONString chatGetUser ChatStore {chatStore} = - maybe "{}" (jsonObject . ("user" .=)) <$> getActiveUser_ chatStore + maybe "{}" userObject <$> getActiveUser_ chatStore -- | returns JSON in the form `{"user": }` or `{"error": ""}` chatCreateUser :: ChatStore -> JSONString -> IO JSONString chatCreateUser ChatStore {chatStore} profileJson = case J.eitherDecodeStrict' $ B.pack profileJson of - Left e -> err e - Right p -> - runExceptT (createUser chatStore p True) >>= \case - Right user -> pure . jsonObject $ "user" .= user - Left e -> err e + Left e -> pure $ err e + Right p -> either err userObject <$> runExceptT (createUser chatStore p True) where - err e = pure . jsonObject $ "error" .= show e + err e = jsonObject $ "error" .= show e + +userObject :: User -> JSONString +userObject user = jsonObject $ "user" .= user chatStart :: ChatStore -> IO ChatController chatStart ChatStore {dbFilePrefix, chatStore} = do @@ -119,33 +117,19 @@ chatStart ChatStore {dbFilePrefix, chatStore} = do pure cc chatSendCmd :: ChatController -> String -> IO JSONString -chatSendCmd cc s = crToJSON (CorrId "") <$> runReaderT (execChatCommand s) cc +chatSendCmd cc s = LB.unpack . J.encode . APIResponse Nothing <$> runReaderT (execChatCommand s) cc chatRecvMsg :: ChatController -> IO JSONString chatRecvMsg ChatController {outputQ} = json <$> atomically (readTBQueue outputQ) where - json (corrId, resp) = crToJSON corrId resp + json (corr, resp) = LB.unpack $ J.encode APIResponse {corr, resp} jsonObject :: J.Series -> JSONString jsonObject = LB.unpack . JE.encodingToLazyByteString . J.pairs -crToJSON :: CorrId -> ChatResponse -> JSONString -crToJSON corrId = LB.unpack . J.encode . crToAPI corrId - -crToAPI :: CorrId -> ChatResponse -> APIResponse -crToAPI (CorrId cId) = \case - CRUserProfile p -> api "profile" $ J.object ["profile" .= p] - r -> api "terminal" $ J.object ["output" .= serializeChatResponse r] - where - corr = if B.null cId then Nothing else Just . B.unpack $ U.encode cId - api tag args = APIResponse {corr, tag, args} - -data APIResponse = APIResponse - { -- | optional correlation ID for async command responses - corr :: Maybe String, - tag :: String, - args :: J.Value - } +data APIResponse = APIResponse {corr :: Maybe CorrId, resp :: ChatResponse} deriving (Generic) -instance ToJSON APIResponse where toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} +instance ToJSON APIResponse where + toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} + toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index fe667fac7d..8e3f8a776d 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -24,7 +24,7 @@ import Data.Text (Text) import Data.Text.Encoding (decodeLatin1, encodeUtf8) import Database.SQLite.Simple.FromField (FromField (..)) import Database.SQLite.Simple.ToField (ToField (..)) -import GHC.Generics +import GHC.Generics (Generic) import Simplex.Chat.Types import Simplex.Messaging.Agent.Store.SQLite (fromTextField_) import Simplex.Messaging.Encoding.String @@ -105,6 +105,8 @@ instance ToJSON MsgContentType where toJSON = strToJSON toEncoding = strToJEncoding +-- TODO - include tag and original JSON into MCUnknown so that information is not lost +-- so when it serializes back it is the same as it was and chat upgrade makes it readable data MsgContent = MCText Text | MCUnknown deriving (Eq, Show) diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index b7091e0d5b..ab9a93afcd 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -1,6 +1,7 @@ {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} @@ -114,6 +115,8 @@ import qualified Control.Exception as E import Control.Monad.Except import Control.Monad.IO.Unlift import Crypto.Random (ChaChaDRG, randomBytesGenerate) +import Data.Aeson (ToJSON) +import qualified Data.Aeson as J import qualified Data.ByteString.Base64 as B64 import Data.ByteString.Char8 (ByteString) import Data.Either (rights) @@ -128,6 +131,7 @@ import Data.Time.Clock (UTCTime, getCurrentTime) import Database.SQLite.Simple (NamedParam (..), Only (..), Query (..), SQLError, (:.) (..)) import qualified Database.SQLite.Simple as DB import Database.SQLite.Simple.QQ (sql) +import GHC.Generics (Generic) import Simplex.Chat.Messages import Simplex.Chat.Migrations.M20220101_initial import Simplex.Chat.Migrations.M20220122_pending_group_messages @@ -138,7 +142,8 @@ import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, InvitationId, MsgMe import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), createSQLiteStore, firstRow, withTransaction) import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) import qualified Simplex.Messaging.Crypto as C -import Simplex.Messaging.Util (bshow, liftIOEither, (<$$>)) +import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) +import Simplex.Messaging.Util (liftIOEither, (<$$>)) import System.FilePath (takeFileName) import UnliftIO.STM @@ -167,7 +172,7 @@ checkConstraint err action = action `E.catch` (pure . Left . handleSQLError err) handleSQLError :: StoreError -> SQLError -> StoreError handleSQLError err e | DB.sqlError e == DB.ErrorConstraint = err - | otherwise = SEInternal $ bshow e + | otherwise = SEInternal $ show e insertedRowId :: DB.Connection -> IO Int64 insertedRowId db = fromOnly . head <$> DB.query_ db "SELECT last_insert_rowid()" @@ -219,11 +224,8 @@ createDirectConnection st userId agentConnId = createContactConnection_ :: DB.Connection -> UserId -> ConnId -> Maybe Int64 -> Int -> IO Connection createContactConnection_ db userId = createConnection_ db userId ConnContact Nothing --- field types coincidentally match, but the first element here is user ID and not connection ID as in ConnectionRow -type InsertedConnectionRow = ConnectionRow - createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> Maybe Int64 -> Int -> IO Connection -createConnection_ db userId connType entityId agentConnId viaContact connLevel = do +createConnection_ db userId connType entityId acId viaContact connLevel = do createdAt <- getCurrentTime DB.execute db @@ -233,25 +235,10 @@ createConnection_ db userId connType entityId agentConnId viaContact connLevel = contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?); |] - (insertConnParams createdAt) + (userId, acId, connLevel, viaContact, ConnNew, connType, ent ConnContact, ent ConnMember, ent ConnSndFile, ent ConnRcvFile, ent ConnUserContact, createdAt) connId <- insertedRowId db - pure Connection {connId, agentConnId, connType, entityId, viaContact, connLevel, connStatus = ConnNew, createdAt} + pure Connection {connId, agentConnId = AgentConnId acId, connType, entityId, viaContact, connLevel, connStatus = ConnNew, createdAt} where - insertConnParams :: UTCTime -> InsertedConnectionRow - insertConnParams createdAt = - ( userId, - agentConnId, - connLevel, - viaContact, - ConnNew, - connType, - ent ConnContact, - ent ConnMember, - ent ConnSndFile, - ent ConnRcvFile, - ent ConnUserContact, - createdAt - ) ent ct = if connType == ct then entityId else Nothing createDirectContact :: StoreMonad m => SQLiteStore -> UserId -> Connection -> Profile -> m () @@ -652,9 +639,9 @@ type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, ConnStatus, ConnType, May type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe UTCTime) toConnection :: ConnectionRow -> Connection -toConnection (connId, agentConnId, connLevel, viaContact, connStatus, connType, contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId, createdAt) = +toConnection (connId, acId, connLevel, viaContact, connStatus, connType, contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId, createdAt) = let entityId = entityId_ connType - in Connection {connId, agentConnId, connLevel, viaContact, connStatus, connType, entityId, createdAt} + in Connection {connId, agentConnId = AgentConnId acId, connLevel, viaContact, connStatus, connType, entityId, createdAt} where entityId_ :: ConnType -> Maybe Int64 entityId_ ConnContact = contactId @@ -795,7 +782,7 @@ getConnectionEntity st User {userId, userContactId} agentConnId = Nothing -> if connType == ConnContact then pure $ RcvDirectMsgConnection c Nothing - else throwError $ SEInternal $ "connection " <> bshow connType <> " without entity" + else throwError $ SEInternal $ "connection " <> show connType <> " without entity" Just entId -> case connType of ConnMember -> uncurry (RcvGroupMsgConnection c) <$> getGroupAndMember_ db entId c @@ -818,7 +805,7 @@ getConnectionEntity st User {userId, userContactId} agentConnId = (userId, agentConnId) connection :: [ConnectionRow] -> Either StoreError Connection connection (connRow : _) = Right $ toConnection connRow - connection _ = Left $ SEConnectionNotFound agentConnId + connection _ = Left . SEConnectionNotFound $ AgentConnId agentConnId getContactRec_ :: DB.Connection -> Int64 -> Connection -> ExceptT StoreError IO Contact getContactRec_ db contactId c = ExceptT $ do toContact contactId c @@ -1432,14 +1419,14 @@ getViaGroupContact st User {userId} GroupMember {groupMemberId} = toContact _ = Nothing createSndFileTransfer :: MonadUnliftIO m => SQLiteStore -> UserId -> Contact -> FilePath -> FileInvitation -> ConnId -> Integer -> m SndFileTransfer -createSndFileTransfer st userId Contact {contactId, localDisplayName = recipientDisplayName} filePath FileInvitation {fileName, fileSize} aConnId chunkSize = +createSndFileTransfer st userId Contact {contactId, localDisplayName = recipientDisplayName} filePath FileInvitation {fileName, fileSize} acId chunkSize = liftIO . withTransaction st $ \db -> do DB.execute db "INSERT INTO files (user_id, contact_id, file_name, file_path, file_size, chunk_size) VALUES (?, ?, ?, ?, ?, ?)" (userId, contactId, fileName, filePath, fileSize, chunkSize) fileId <- insertedRowId db - Connection {connId} <- createSndFileConnection_ db userId fileId aConnId + Connection {connId} <- createSndFileConnection_ db userId fileId acId let fileStatus = FSNew DB.execute db "INSERT INTO snd_files (file_id, file_status, connection_id) VALUES (?, ?, ?)" (fileId, fileStatus, connId) - pure SndFileTransfer {fileId, fileName, filePath, fileSize, chunkSize, recipientDisplayName, connId, fileStatus, agentConnId = AgentConnId aConnId} + pure SndFileTransfer {fileId, fileName, filePath, fileSize, chunkSize, recipientDisplayName, connId, fileStatus, agentConnId = AgentConnId acId} createSndGroupFileTransfer :: MonadUnliftIO m => SQLiteStore -> UserId -> GroupInfo -> [(GroupMember, ConnId, FileInvitation)] -> FilePath -> Integer -> Integer -> m Int64 createSndGroupFileTransfer st userId GroupInfo {groupId} ms filePath fileSize chunkSize = @@ -1990,7 +1977,7 @@ createWithRandomBytes size gVar create = tryCreate 3 Right x -> pure $ Right x Left e | DB.sqlError e == DB.ErrorConstraint -> tryCreate (n - 1) - | otherwise -> pure . Left . SEInternal $ bshow e + | otherwise -> pure . Left . SEInternal $ show e randomBytes :: TVar ChaChaDRG -> Int -> IO ByteString randomBytes gVar n = B64.encode <$> (atomically . stateTVar gVar $ randomBytesGenerate n) @@ -2012,9 +1999,13 @@ data StoreError | SERcvFileNotFound Int64 | SEFileNotFound Int64 | SERcvFileInvalid Int64 - | SEConnectionNotFound ConnId + | SEConnectionNotFound AgentConnId | SEIntroNotFound | SEUniqueID - | SEInternal ByteString + | SEInternal String | SENoMsgDelivery Int64 AgentMsgId - deriving (Show, Exception) + deriving (Show, Exception, Generic) + +instance ToJSON StoreError where + toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "SE" + toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "SE" diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 1bfcdfe32b..014ca47d31 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -1,17 +1,21 @@ +{-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE UndecidableInstances #-} module Simplex.Chat.Types where -import Data.Aeson (FromJSON, ToJSON, (.:), (.=)) +import Data.Aeson (FromJSON, ToJSON) import qualified Data.Aeson as J import qualified Data.Aeson.Encoding as JE import qualified Data.Aeson.Types as JT @@ -21,16 +25,17 @@ import qualified Data.ByteString.Char8 as B import Data.Int (Int64) import Data.Text (Text) import Data.Time.Clock (UTCTime) -import Data.Typeable (Typeable) +import Data.Typeable import Database.SQLite.Simple (ResultError (..), SQLData (..)) import Database.SQLite.Simple.FromField (FieldParser, FromField (..), returnError) import Database.SQLite.Simple.Internal (Field (..)) import Database.SQLite.Simple.Ok (Ok (Ok)) import Database.SQLite.Simple.ToField (ToField (..)) -import GHC.Generics +import GHC.Generics (Generic) import Simplex.Messaging.Agent.Protocol (ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId) import Simplex.Messaging.Agent.Store.SQLite (fromTextField_) import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) import Simplex.Messaging.Util ((<$?>)) class IsContact a where @@ -57,7 +62,7 @@ data User = User } deriving (Show, Generic, FromJSON) -instance ToJSON User where toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} +instance ToJSON User where toEncoding = J.genericToEncoding J.defaultOptions type UserId = Int64 @@ -68,13 +73,17 @@ data Contact = Contact activeConn :: Connection, viaGroup :: Maybe Int64 } - deriving (Eq, Show) + deriving (Eq, Show, Generic, FromJSON) + +instance ToJSON Contact where + toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} + toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} contactConn :: Contact -> Connection contactConn = activeConn contactConnId :: Contact -> ConnId -contactConnId Contact {activeConn = Connection {agentConnId}} = agentConnId +contactConnId Contact {activeConn} = aConnId activeConn data UserContact = UserContact { userContactLinkId :: Int64, @@ -96,8 +105,10 @@ type ContactName = Text type GroupName = Text -data Group = Group GroupInfo [GroupMember] - deriving (Eq, Show) +data Group = Group {groupInfo :: GroupInfo, members :: [GroupMember]} + deriving (Eq, Show, Generic) + +instance ToJSON Group where toEncoding = J.genericToEncoding J.defaultOptions data GroupInfo = GroupInfo { groupId :: Int64, @@ -105,7 +116,9 @@ data GroupInfo = GroupInfo groupProfile :: GroupProfile, membership :: GroupMember } - deriving (Eq, Show) + deriving (Eq, Show, Generic, FromJSON) + +instance ToJSON GroupInfo where toEncoding = J.genericToEncoding J.defaultOptions groupName :: GroupInfo -> GroupName groupName GroupInfo {localDisplayName = g} = g @@ -116,7 +129,7 @@ data Profile = Profile } deriving (Eq, Show, Generic, FromJSON) -instance ToJSON Profile where toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} +instance ToJSON Profile where toEncoding = J.genericToEncoding J.defaultOptions data GroupProfile = GroupProfile { displayName :: GroupName, @@ -124,7 +137,7 @@ data GroupProfile = GroupProfile } deriving (Eq, Show, Generic, FromJSON) -instance ToJSON GroupProfile where toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} +instance ToJSON GroupProfile where toEncoding = J.genericToEncoding J.defaultOptions data GroupInvitation = GroupInvitation { fromMember :: MemberIdRole, @@ -134,7 +147,7 @@ data GroupInvitation = GroupInvitation } deriving (Eq, Show, Generic, FromJSON) -instance ToJSON GroupInvitation where toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} +instance ToJSON GroupInvitation where toEncoding = J.genericToEncoding J.defaultOptions data MemberIdRole = MemberIdRole { memberId :: MemberId, @@ -142,7 +155,7 @@ data MemberIdRole = MemberIdRole } deriving (Eq, Show, Generic, FromJSON) -instance ToJSON MemberIdRole where toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} +instance ToJSON MemberIdRole where toEncoding = J.genericToEncoding J.defaultOptions data IntroInvitation = IntroInvitation { groupConnReq :: ConnReqInvitation, @@ -150,7 +163,7 @@ data IntroInvitation = IntroInvitation } deriving (Eq, Show, Generic, FromJSON) -instance ToJSON IntroInvitation where toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} +instance ToJSON IntroInvitation where toEncoding = J.genericToEncoding J.defaultOptions data MemberInfo = MemberInfo { memberId :: MemberId, @@ -159,7 +172,7 @@ data MemberInfo = MemberInfo } deriving (Eq, Show, Generic, FromJSON) -instance ToJSON MemberInfo where toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} +instance ToJSON MemberInfo where toEncoding = J.genericToEncoding J.defaultOptions memberInfo :: GroupMember -> MemberInfo memberInfo GroupMember {memberId, memberRole, memberProfile} = @@ -185,15 +198,17 @@ data GroupMember = GroupMember memberContactId :: Maybe Int64, activeConn :: Maybe Connection } - deriving (Eq, Show) + deriving (Eq, Show, Generic, FromJSON) + +instance ToJSON GroupMember where + toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} + toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} memberConn :: GroupMember -> Maybe Connection memberConn = activeConn memberConnId :: GroupMember -> Maybe ConnId -memberConnId GroupMember {activeConn} = case activeConn of - Just Connection {agentConnId} -> Just agentConnId - Nothing -> Nothing +memberConnId GroupMember {activeConn} = aConnId <$> activeConn data NewGroupMember = NewGroupMember { memInfo :: MemberInfo, @@ -224,8 +239,15 @@ instance ToJSON MemberId where toJSON = strToJSON toEncoding = strToJEncoding -data InvitedBy = IBContact Int64 | IBUser | IBUnknown - deriving (Eq, Show) +data InvitedBy = IBContact {byContactId :: Int64} | IBUser | IBUnknown + deriving (Eq, Show, Generic) + +instance FromJSON InvitedBy where + parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "IB" + +instance ToJSON InvitedBy where + toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "IB" + toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "IB" toInvitedBy :: Int64 -> Maybe Int64 -> InvitedBy toInvitedBy userCtId (Just ctId) @@ -311,26 +333,30 @@ data GroupMemberCategory | GCPostMember -- member who joined after the user to whom the user was introduced (user receives x.grp.mem.new announcing these members and then x.grp.mem.fwd with invitation from these members) deriving (Eq, Show) -instance FromField GroupMemberCategory where fromField = fromTextField_ memberCategoryT +instance FromField GroupMemberCategory where fromField = fromTextField_ decodeText -instance ToField GroupMemberCategory where toField = toField . serializeMemberCategory +instance ToField GroupMemberCategory where toField = toField . encodeText -memberCategoryT :: Text -> Maybe GroupMemberCategory -memberCategoryT = \case - "user" -> Just GCUserMember - "invitee" -> Just GCInviteeMember - "host" -> Just GCHostMember - "pre" -> Just GCPreMember - "post" -> Just GCPostMember - _ -> Nothing +instance FromJSON GroupMemberCategory where parseJSON = textParseJSON "GroupMemberCategory" -serializeMemberCategory :: GroupMemberCategory -> Text -serializeMemberCategory = \case - GCUserMember -> "user" - GCInviteeMember -> "invitee" - GCHostMember -> "host" - GCPreMember -> "pre" - GCPostMember -> "post" +instance ToJSON GroupMemberCategory where + toJSON = J.String . encodeText + toEncoding = JE.text . encodeText + +instance TextEncoding GroupMemberCategory where + decodeText = \case + "user" -> Just GCUserMember + "invitee" -> Just GCInviteeMember + "host" -> Just GCHostMember + "pre" -> Just GCPreMember + "post" -> Just GCPostMember + _ -> Nothing + encodeText = \case + GCUserMember -> "user" + GCInviteeMember -> "invitee" + GCHostMember -> "host" + GCPreMember -> "pre" + GCPostMember -> "post" data GroupMemberStatus = GSMemRemoved -- member who was removed from the group @@ -346,9 +372,15 @@ data GroupMemberStatus | GSMemCreator -- user member that created the group (only GCUserMember) deriving (Eq, Show, Ord) -instance FromField GroupMemberStatus where fromField = fromTextField_ memberStatusT +instance FromField GroupMemberStatus where fromField = fromTextField_ decodeText -instance ToField GroupMemberStatus where toField = toField . serializeMemberStatus +instance ToField GroupMemberStatus where toField = toField . encodeText + +instance FromJSON GroupMemberStatus where parseJSON = textParseJSON "GroupMemberStatus" + +instance ToJSON GroupMemberStatus where + toJSON = J.String . encodeText + toEncoding = JE.text . encodeText memberActive :: GroupMember -> Bool memberActive m = case memberStatus m of @@ -378,34 +410,32 @@ memberCurrent m = case memberStatus m of GSMemComplete -> True GSMemCreator -> True -memberStatusT :: Text -> Maybe GroupMemberStatus -memberStatusT = \case - "removed" -> Just GSMemRemoved - "left" -> Just GSMemLeft - "deleted" -> Just GSMemGroupDeleted - "invited" -> Just GSMemInvited - "introduced" -> Just GSMemIntroduced - "intro-inv" -> Just GSMemIntroInvited - "accepted" -> Just GSMemAccepted - "announced" -> Just GSMemAnnounced - "connected" -> Just GSMemConnected - "complete" -> Just GSMemComplete - "creator" -> Just GSMemCreator - _ -> Nothing - -serializeMemberStatus :: GroupMemberStatus -> Text -serializeMemberStatus = \case - GSMemRemoved -> "removed" - GSMemLeft -> "left" - GSMemGroupDeleted -> "deleted" - GSMemInvited -> "invited" - GSMemIntroduced -> "introduced" - GSMemIntroInvited -> "intro-inv" - GSMemAccepted -> "accepted" - GSMemAnnounced -> "announced" - GSMemConnected -> "connected" - GSMemComplete -> "complete" - GSMemCreator -> "creator" +instance TextEncoding GroupMemberStatus where + decodeText = \case + "removed" -> Just GSMemRemoved + "left" -> Just GSMemLeft + "deleted" -> Just GSMemGroupDeleted + "invited" -> Just GSMemInvited + "introduced" -> Just GSMemIntroduced + "intro-inv" -> Just GSMemIntroInvited + "accepted" -> Just GSMemAccepted + "announced" -> Just GSMemAnnounced + "connected" -> Just GSMemConnected + "complete" -> Just GSMemComplete + "creator" -> Just GSMemCreator + _ -> Nothing + encodeText = \case + GSMemRemoved -> "removed" + GSMemLeft -> "left" + GSMemGroupDeleted -> "deleted" + GSMemInvited -> "invited" + GSMemIntroduced -> "introduced" + GSMemIntroInvited -> "intro-inv" + GSMemAccepted -> "accepted" + GSMemAnnounced -> "announced" + GSMemConnected -> "connected" + GSMemComplete -> "complete" + GSMemCreator -> "creator" data SndFileTransfer = SndFileTransfer { fileId :: FileTransferId, @@ -418,7 +448,9 @@ data SndFileTransfer = SndFileTransfer agentConnId :: AgentConnId, fileStatus :: FileStatus } - deriving (Eq, Show) + deriving (Eq, Show, Generic) + +instance ToJSON SndFileTransfer where toEncoding = J.genericToEncoding J.defaultOptions type FileTransferId = Int64 @@ -427,24 +459,9 @@ data FileInvitation = FileInvitation fileSize :: Integer, fileConnReq :: ConnReqInvitation } - deriving (Eq, Show, Generic) + deriving (Eq, Show, Generic, FromJSON) -instance FromJSON FileInvitation where - parseJSON (J.Object v) = FileInvitation <$> v .: "fileName" <*> v .: "fileSize" <*> v .: "fileConnReq" - parseJSON invalid = JT.prependFailure "bad FileInvitation, " (JT.typeMismatch "Object" invalid) - -instance ToJSON FileInvitation where - toJSON (FileInvitation fileName fileSize fileConnReq) = - J.object - [ "fileName" .= fileName, - "fileSize" .= fileSize, - "fileConnReq" .= fileConnReq - ] - toEncoding (FileInvitation fileName fileSize fileConnReq) = - J.pairs $ - "fileName" .= fileName - <> "fileSize" .= fileSize - <> "fileConnReq" .= fileConnReq +instance ToJSON FileInvitation where toEncoding = J.genericToEncoding J.defaultOptions data RcvFileTransfer = RcvFileTransfer { fileId :: FileTransferId, @@ -455,7 +472,7 @@ data RcvFileTransfer = RcvFileTransfer } deriving (Eq, Show, Generic, FromJSON) -instance ToJSON RcvFileTransfer where toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} +instance ToJSON RcvFileTransfer where toEncoding = J.genericToEncoding J.defaultOptions data RcvFileStatus = RFSNew @@ -463,38 +480,14 @@ data RcvFileStatus | RFSConnected RcvFileInfo | RFSComplete RcvFileInfo | RFSCancelled RcvFileInfo - deriving (Eq, Show) + deriving (Eq, Show, Generic) instance FromJSON RcvFileStatus where - parseJSON = J.withObject "RcvFileStatus" $ \v -> do - let rfs mk = mk <$> v .: "fileInfo" - v .: "status" >>= \case - ("new" :: Text) -> pure RFSNew - "accepted" -> rfs RFSAccepted - "connected" -> rfs RFSConnected - "complete" -> rfs RFSComplete - "cancelled" -> rfs RFSCancelled - _ -> fail "bad RcvFileStatus" + parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "RFS" instance ToJSON RcvFileStatus where - toJSON s = J.object $ ["status" .= rfsTag s, "fileInfo" .= rfsInfo s] - toEncoding s = J.pairs $ ("status" .= rfsTag s <> "fileInfo" .= rfsInfo s) - -rfsTag :: RcvFileStatus -> Text -rfsTag = \case - RFSNew -> "new" - RFSAccepted _ -> "accepted" - RFSConnected _ -> "connected" - RFSComplete _ -> "complete" - RFSCancelled _ -> "cancelled" - -rfsInfo :: RcvFileStatus -> Maybe RcvFileInfo -rfsInfo = \case - RFSNew -> Nothing - RFSAccepted info -> Just info - RFSConnected info -> Just info - RFSComplete info -> Just info - RFSCancelled info -> Just info + toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "RFS" + toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "RFS" data RcvFileInfo = RcvFileInfo { filePath :: FilePath, @@ -503,7 +496,7 @@ data RcvFileInfo = RcvFileInfo } deriving (Eq, Show, Generic, FromJSON) -instance ToJSON RcvFileInfo where toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} +instance ToJSON RcvFileInfo where toEncoding = J.genericToEncoding J.defaultOptions newtype AgentConnId = AgentConnId ConnId deriving (Eq, Show) @@ -524,38 +517,39 @@ instance FromField AgentConnId where fromField f = AgentConnId <$> fromField f instance ToField AgentConnId where toField (AgentConnId m) = toField m -data FileTransfer = FTSnd [SndFileTransfer] | FTRcv RcvFileTransfer - deriving (Show) +data FileTransfer = FTSnd {sndFileTransfers :: [SndFileTransfer]} | FTRcv RcvFileTransfer + deriving (Show, Generic) + +instance ToJSON FileTransfer where + toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "FT" + toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "FT" data FileStatus = FSNew | FSAccepted | FSConnected | FSComplete | FSCancelled deriving (Eq, Ord, Show) -instance FromField FileStatus where fromField = fromTextField_ fileStatusT +instance FromField FileStatus where fromField = fromTextField_ decodeText -instance ToField FileStatus where toField = toField . serializeFileStatus +instance ToField FileStatus where toField = toField . encodeText -instance FromJSON FileStatus where - parseJSON = J.withText "FileStatus" $ maybe (fail "bad FileStatus") pure . fileStatusT +instance FromJSON FileStatus where parseJSON = textParseJSON "FileStatus" instance ToJSON FileStatus where - toJSON = J.String . serializeFileStatus - toEncoding = JE.text . serializeFileStatus + toJSON = J.String . encodeText + toEncoding = JE.text . encodeText -fileStatusT :: Text -> Maybe FileStatus -fileStatusT = \case - "new" -> Just FSNew - "accepted" -> Just FSAccepted - "connected" -> Just FSConnected - "complete" -> Just FSComplete - "cancelled" -> Just FSCancelled - _ -> Nothing - -serializeFileStatus :: FileStatus -> Text -serializeFileStatus = \case - FSNew -> "new" - FSAccepted -> "accepted" - FSConnected -> "connected" - FSComplete -> "complete" - FSCancelled -> "cancelled" +instance TextEncoding FileStatus where + decodeText = \case + "new" -> Just FSNew + "accepted" -> Just FSAccepted + "connected" -> Just FSConnected + "complete" -> Just FSComplete + "cancelled" -> Just FSCancelled + _ -> Nothing + encodeText = \case + FSNew -> "new" + FSAccepted -> "accepted" + FSConnected -> "connected" + FSComplete -> "complete" + FSCancelled -> "cancelled" data RcvChunkStatus = RcvChunkOk | RcvChunkFinal | RcvChunkDuplicate | RcvChunkError deriving (Eq, Show) @@ -566,7 +560,7 @@ type ConnReqContact = ConnectionRequestUri 'CMContact data Connection = Connection { connId :: Int64, - agentConnId :: ConnId, + agentConnId :: AgentConnId, connLevel :: Int, viaContact :: Maybe Int64, connType :: ConnType, @@ -574,7 +568,14 @@ data Connection = Connection entityId :: Maybe Int64, -- contact, group member, file ID or user contact ID createdAt :: UTCTime } - deriving (Eq, Show) + deriving (Eq, Show, Generic, FromJSON) + +aConnId :: Connection -> ConnId +aConnId Connection {agentConnId = AgentConnId cId} = cId + +instance ToJSON Connection where + toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} + toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} data ConnStatus = -- | connection is created by initiating party with agent NEW command (createConnection) @@ -593,54 +594,62 @@ data ConnStatus ConnDeleted deriving (Eq, Show) -instance FromField ConnStatus where fromField = fromTextField_ connStatusT +instance FromField ConnStatus where fromField = fromTextField_ decodeText -instance ToField ConnStatus where toField = toField . serializeConnStatus +instance ToField ConnStatus where toField = toField . encodeText -connStatusT :: Text -> Maybe ConnStatus -connStatusT = \case - "new" -> Just ConnNew - "joined" -> Just ConnJoined - "requested" -> Just ConnRequested - "accepted" -> Just ConnAccepted - "snd-ready" -> Just ConnSndReady - "ready" -> Just ConnReady - "deleted" -> Just ConnDeleted - _ -> Nothing +instance FromJSON ConnStatus where parseJSON = textParseJSON "ConnStatus" -serializeConnStatus :: ConnStatus -> Text -serializeConnStatus = \case - ConnNew -> "new" - ConnJoined -> "joined" - ConnRequested -> "requested" - ConnAccepted -> "accepted" - ConnSndReady -> "snd-ready" - ConnReady -> "ready" - ConnDeleted -> "deleted" +instance ToJSON ConnStatus where + toJSON = J.String . encodeText + toEncoding = JE.text . encodeText + +instance TextEncoding ConnStatus where + decodeText = \case + "new" -> Just ConnNew + "joined" -> Just ConnJoined + "requested" -> Just ConnRequested + "accepted" -> Just ConnAccepted + "snd-ready" -> Just ConnSndReady + "ready" -> Just ConnReady + "deleted" -> Just ConnDeleted + _ -> Nothing + encodeText = \case + ConnNew -> "new" + ConnJoined -> "joined" + ConnRequested -> "requested" + ConnAccepted -> "accepted" + ConnSndReady -> "snd-ready" + ConnReady -> "ready" + ConnDeleted -> "deleted" data ConnType = ConnContact | ConnMember | ConnSndFile | ConnRcvFile | ConnUserContact deriving (Eq, Show) -instance FromField ConnType where fromField = fromTextField_ connTypeT +instance FromField ConnType where fromField = fromTextField_ decodeText -instance ToField ConnType where toField = toField . serializeConnType +instance ToField ConnType where toField = toField . encodeText -connTypeT :: Text -> Maybe ConnType -connTypeT = \case - "contact" -> Just ConnContact - "member" -> Just ConnMember - "snd_file" -> Just ConnSndFile - "rcv_file" -> Just ConnRcvFile - "user_contact" -> Just ConnUserContact - _ -> Nothing +instance FromJSON ConnType where parseJSON = textParseJSON "ConnType" -serializeConnType :: ConnType -> Text -serializeConnType = \case - ConnContact -> "contact" - ConnMember -> "member" - ConnSndFile -> "snd_file" - ConnRcvFile -> "rcv_file" - ConnUserContact -> "user_contact" +instance ToJSON ConnType where + toJSON = J.String . encodeText + toEncoding = JE.text . encodeText + +instance TextEncoding ConnType where + decodeText = \case + "contact" -> Just ConnContact + "member" -> Just ConnMember + "snd_file" -> Just ConnSndFile + "rcv_file" -> Just ConnRcvFile + "user_contact" -> Just ConnUserContact + _ -> Nothing + encodeText = \case + ConnContact -> "contact" + ConnMember -> "member" + ConnSndFile -> "snd_file" + ConnRcvFile -> "rcv_file" + ConnUserContact -> "user_contact" data NewConnection = NewConnection { agentConnId :: ByteString, @@ -695,3 +704,10 @@ serializeIntroStatus = \case data Notification = Notification {title :: Text, text :: Text} type JSONString = String + +class TextEncoding a where + encodeText :: a -> Text + decodeText :: Text -> Maybe a + +textParseJSON :: TextEncoding a => String -> J.Value -> JT.Parser a +textParseJSON name = J.withText name $ maybe (fail $ "bad " <> name) pure . decodeText diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 8ae4758537..70857e0020 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -34,7 +34,7 @@ serializeChatResponse = unlines . map unStyle . responseToView "" responseToView :: String -> ChatResponse -> [StyledString] responseToView cmd = \case CRNewChatItem (AChatItem _ _ chat item) -> viewChatItem chat item - CRCommandAccepted _ -> r [] + CRCmdAccepted _ -> r [] CRChatHelp section -> case section of HSMain -> r chatHelpInfo HSFiles -> r filesHelpInfo @@ -361,7 +361,7 @@ sendingFile_ status ft@SndFileTransfer {recipientDisplayName = c} = [status <> " sending " <> sndFile ft <> " to " <> ttyContact c] sndFile :: SndFileTransfer -> StyledString -sndFile SndFileTransfer {fileId, fileName} = fileTransfer fileId fileName +sndFile SndFileTransfer {fileId, fileName} = fileTransferStr fileId fileName viewReceivedFileInvitation :: StyledString -> CIMetaProps -> RcvFileTransfer -> MsgIntegrity -> [StyledString] viewReceivedFileInvitation from meta ft = receivedWithTime_ from meta (receivedFileInvitation_ ft) @@ -389,10 +389,10 @@ receivingFile_ status ft@RcvFileTransfer {senderDisplayName = c} = [status <> " receiving " <> rcvFile ft <> " from " <> ttyContact c] rcvFile :: RcvFileTransfer -> StyledString -rcvFile RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}} = fileTransfer fileId fileName +rcvFile RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}} = fileTransferStr fileId fileName -fileTransfer :: Int64 -> String -> StyledString -fileTransfer fileId fileName = "file " <> sShow fileId <> " (" <> ttyFilePath fileName <> ")" +fileTransferStr :: Int64 -> String -> StyledString +fileTransferStr fileId fileName = "file " <> sShow fileId <> " (" <> ttyFilePath fileName <> ")" viewFileTransferStatus :: (FileTransfer, [Integer]) -> [StyledString] viewFileTransferStatus (FTSnd [ft@SndFileTransfer {fileStatus, fileSize, chunkSize}], chunksNum) = diff --git a/stack.yaml b/stack.yaml index 8942dffea1..0e2d618983 100644 --- a/stack.yaml +++ b/stack.yaml @@ -41,7 +41,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: b777a4fd93f888d549edf1877583fb7fc0e0196f + commit: 6fe3bfa980847c074b4cb0b9f3ea01cc5e6c567b # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/haskell-terminal commit: f708b00009b54890172068f168bf98508ffcd495 diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index a8aff63628..5ee996bdbd 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PostfixOperators #-}