JSON encoding for ChatResponse and all other types used in mobile API (#226)

* JSON encoding for ChatResponse and all other types used in mobile API

* omit null corrId in response, refactor

* more JSON field names
This commit is contained in:
Evgeny Poberezkin
2022-01-26 21:20:08 +00:00
committed by GitHub
parent ecb5b0fdeb
commit 0ba4598ca2
12 changed files with 482 additions and 399 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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,

View File

@@ -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": <user object>}` or `{}`
chatGetUser :: ChatStore -> IO JSONString
chatGetUser ChatStore {chatStore} =
maybe "{}" (jsonObject . ("user" .=)) <$> getActiveUser_ chatStore
maybe "{}" userObject <$> getActiveUser_ chatStore
-- | returns JSON in the form `{"user": <user object>}` or `{"error": "<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}

View File

@@ -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)

View File

@@ -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"

View File

@@ -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

View File

@@ -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) =