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:
committed by
GitHub
parent
ecb5b0fdeb
commit
0ba4598ca2
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) =
|
||||
|
||||
Reference in New Issue
Block a user