From fc9db9c38182e9b9fb2edae472be6d18ddfbc9e2 Mon Sep 17 00:00:00 2001 From: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> Date: Thu, 5 Oct 2023 21:49:20 +0300 Subject: [PATCH] core: add FromJSON instance to ChatResponse (#3129) * Start adding FromJSON instances to ChatResponse * progress * FromJSON instance for ChatResponse compiles * restore removed encodings * remove comment * diff * update simplexmq, use TH for JSON --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat.hs | 8 +- src/Simplex/Chat/Call.hs | 20 +- src/Simplex/Chat/Controller.hs | 101 ++++++---- src/Simplex/Chat/Markdown.hs | 25 ++- src/Simplex/Chat/Messages.hs | 243 +++++++++++++++++++++---- src/Simplex/Chat/Messages/CIContent.hs | 110 +++++------ src/Simplex/Chat/Protocol.hs | 3 + src/Simplex/Chat/Remote.hs | 2 +- src/Simplex/Chat/Remote/Types.hs | 8 +- src/Simplex/Chat/Store/Profiles.hs | 7 +- src/Simplex/Chat/Store/Shared.hs | 5 +- src/Simplex/Chat/Types.hs | 134 +++++++++----- src/Simplex/Chat/Types/Preferences.hs | 7 +- src/Simplex/Chat/Types/Util.hs | 3 + src/Simplex/Chat/View.hs | 10 +- stack.yaml | 2 +- 18 files changed, 483 insertions(+), 209 deletions(-) diff --git a/cabal.project b/cabal.project index f5bb879762..03b3bc8100 100644 --- a/cabal.project +++ b/cabal.project @@ -9,7 +9,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 753a6c7542c3764fda9ce3f4c4cdc9f2329816d3 + tag: 96a38505d63ec9a12096991e7725b250e397af72 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index b6ca36e313..748da0363c 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."ec1b72cb8013a65a5d9783104a47ae44f5730089" = "1lz5rvgxp242zg95r9zd9j50y45314cf8nfpjg1qsa55nrk2w19b"; + "https://github.com/simplex-chat/simplexmq.git"."96a38505d63ec9a12096991e7725b250e397af72" = "0kllakklvfrbpjlk6zi5mbxqm1prp6xdwyh2y4fw9n6c8b76is98"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/kazu-yamamoto/http2.git"."b5a1b7200cf5bc7044af34ba325284271f6dff25" = "0dqb50j57an64nf4qcf5vcz4xkd1vzvghvf8bk529c1k30r9nfzb"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "0kiwhvml42g9anw4d2v0zd1fpc790pj9syg5x3ik4l97fnkbbwpp"; diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 29f69d99da..bcd23f44ef 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -469,11 +469,11 @@ processChatCommand = \case DeleteUser uName delSMPQueues viewPwd_ -> withUserName uName $ \userId -> APIDeleteUser userId delSMPQueues viewPwd_ StartChat subConns enableExpireCIs startXFTPWorkers -> withUser' $ \_ -> asks agentAsync >>= readTVarIO >>= \case - Just _ -> pure CRChatRunning - _ -> checkStoreNotChanged $ startChatController subConns enableExpireCIs startXFTPWorkers $> CRChatStarted + Just _ -> pure $ CRChatRunning Nothing + _ -> checkStoreNotChanged $ startChatController subConns enableExpireCIs startXFTPWorkers $> CRChatStarted Nothing APIStopChat -> do ask >>= stopChatController - pure CRChatStopped + pure $ CRChatStopped Nothing APIActivateChat -> withUser $ \_ -> do restoreCalls withAgent foregroundAgent @@ -2814,7 +2814,7 @@ processAgentMessageNoConn = \case DISCONNECT p h -> hostEvent $ CRHostDisconnected p h DOWN srv conns -> serverEvent srv conns CRContactsDisconnected "disconnected" UP srv conns -> serverEvent srv conns CRContactsSubscribed "connected" - SUSPENDED -> toView CRChatSuspended + SUSPENDED -> toView $ CRChatSuspended Nothing DEL_USER agentUserId -> toView $ CRAgentUserDeleted agentUserId where hostEvent :: ChatResponse -> m () diff --git a/src/Simplex/Chat/Call.hs b/src/Simplex/Chat/Call.hs index 7a738512bd..7e6e60c8f5 100644 --- a/src/Simplex/Chat/Call.hs +++ b/src/Simplex/Chat/Call.hs @@ -49,6 +49,9 @@ data CallStateTag | CSTCallNegotiated deriving (Show, Generic) +instance FromJSON CallStateTag where + parseJSON = J.genericParseJSON . enumJSON $ dropPrefix "CSTCall" + instance ToJSON CallStateTag where toJSON = J.genericToJSON . enumJSON $ dropPrefix "CSTCall" toEncoding = J.genericToEncoding . enumJSON $ dropPrefix "CSTCall" @@ -132,7 +135,7 @@ data RcvCallInvitation = RcvCallInvitation sharedKey :: Maybe C.Key, callTs :: UTCTime } - deriving (Show, Generic) + deriving (Show, Generic, FromJSON) instance ToJSON RcvCallInvitation where toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} @@ -157,10 +160,7 @@ data CallInvitation = CallInvitation { callType :: CallType, callDhPubKey :: Maybe C.PublicKeyX25519 } - deriving (Eq, Show, Generic) - -instance FromJSON CallInvitation where - parseJSON = J.genericParseJSON J.defaultOptions {J.omitNothingFields = True} + deriving (Eq, Show, Generic, FromJSON) instance ToJSON CallInvitation where toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} @@ -190,10 +190,7 @@ data CallOffer = CallOffer rtcSession :: WebRTCSession, callDhPubKey :: Maybe C.PublicKeyX25519 } - deriving (Eq, Show, Generic) - -instance FromJSON CallOffer where - parseJSON = J.genericParseJSON J.defaultOptions {J.omitNothingFields = True} + deriving (Eq, Show, Generic, FromJSON) instance ToJSON CallOffer where toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} @@ -203,10 +200,7 @@ data WebRTCCallOffer = WebRTCCallOffer { callType :: CallType, rtcSession :: WebRTCSession } - deriving (Eq, Show, Generic) - -instance FromJSON WebRTCCallOffer where - parseJSON = J.genericParseJSON J.defaultOptions {J.omitNothingFields = True} + deriving (Eq, Show, Generic, FromJSON) instance ToJSON WebRTCCallOffer where toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index c6c813b743..986eaf073e 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -1,4 +1,5 @@ {-# LANGUAGE ConstraintKinds #-} +{-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-} @@ -12,6 +13,7 @@ {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE StrictData #-} {-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} module Simplex.Chat.Controller where @@ -24,11 +26,13 @@ import Control.Monad.Reader import Crypto.Random (ChaChaDRG) import Data.Aeson (FromJSON (..), ToJSON (..), (.:), (.:?)) import qualified Data.Aeson as J +import qualified Data.Aeson.TH as JQ import qualified Data.Aeson.Types as JT import qualified Data.Attoparsec.ByteString.Char8 as A import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Char (ord) +import Data.Constraint (Dict (..)) import Data.Int (Int64) import Data.List.NonEmpty (NonEmpty) import Data.Map.Strict (Map) @@ -64,7 +68,7 @@ import qualified Simplex.Messaging.Crypto.File as CF import Simplex.Messaging.Encoding.String import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus) import Simplex.Messaging.Parsers (dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON) -import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType, CorrId, MsgFlags, NtfServer, ProtoServerWithAuth, ProtocolTypeI, QueueId, SProtocolType, SubscriptionMode (..), UserProtocol, XFTPServerWithAuth) +import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), CorrId, MsgFlags, NtfServer, ProtoServerWithAuth, ProtocolTypeI, QueueId, SProtocolType, SubscriptionMode (..), UserProtocol, XFTPServerWithAuth, userProtocol) import Simplex.Messaging.TMap (TMap) import Simplex.Messaging.Transport (simplexMQVersion) import Simplex.Messaging.Transport.Client (TransportHost) @@ -200,6 +204,9 @@ data ChatController = ChatController data HelpSection = HSMain | HSFiles | HSGroups | HSContacts | HSMyAddress | HSIncognito | HSMarkdown | HSMessages | HSSettings | HSDatabase deriving (Show, Generic) +instance FromJSON HelpSection where + parseJSON = J.genericParseJSON . enumJSON $ dropPrefix "HS" + instance ToJSON HelpSection where toJSON = J.genericToJSON . enumJSON $ dropPrefix "HS" toEncoding = J.genericToEncoding . enumJSON $ dropPrefix "HS" @@ -438,10 +445,10 @@ data ChatCommand data ChatResponse = CRActiveUser {user :: User} | CRUsersList {users :: [UserInfo]} - | CRChatStarted - | CRChatRunning - | CRChatStopped - | CRChatSuspended + | CRChatStarted {_nullary :: Maybe Int} + | CRChatRunning {_nullary :: Maybe Int} + | CRChatStopped {_nullary :: Maybe Int} + | CRChatSuspended {_nullary :: Maybe Int} | CRApiChats {user :: User, chats :: [AChat]} | CRChats {chats :: [AChat]} | CRApiChat {user :: User, chat :: AChat} @@ -605,7 +612,7 @@ data ChatResponse | CRRemoteHostDeleted {remoteHostId :: RemoteHostId} | CRRemoteCtrlList {remoteCtrls :: [RemoteCtrlInfo]} | CRRemoteCtrlRegistered {remoteCtrlId :: RemoteCtrlId} - | CRRemoteCtrlStarted + | CRRemoteCtrlStarted {_nullary :: Maybe Int} | CRRemoteCtrlAnnounce {fingerprint :: C.KeyHash} -- unregistered fingerprint, needs confirmation | CRRemoteCtrlFound {remoteCtrl :: RemoteCtrl} -- registered fingerprint, may connect | CRRemoteCtrlAccepted {remoteCtrlId :: RemoteCtrlId} @@ -629,7 +636,7 @@ data ChatResponse | CRChatError {user_ :: Maybe User, chatError :: ChatError} | CRArchiveImported {archiveErrors :: [ArchiveError]} | CRTimedAction {action :: String, durationMilliseconds :: Int64} - deriving (Show, Generic) + deriving (Show) logResponseToFile :: ChatResponse -> Bool logResponseToFile = \case @@ -650,17 +657,12 @@ logResponseToFile = \case CRMessageError {} -> True _ -> False -instance FromJSON ChatResponse where - parseJSON todo = pure $ CRCmdOk Nothing -- TODO: actually use the instances - -instance ToJSON ChatResponse where - toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "CR" - toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "CR" - data RemoteCtrlOOB = RemoteCtrlOOB { caFingerprint :: C.KeyHash } - deriving (Show, Generic, ToJSON) + deriving (Show, Generic, FromJSON) + +instance ToJSON RemoteCtrlOOB where toEncoding = J.genericToEncoding J.defaultOptions data RemoteHostInfo = RemoteHostInfo { remoteHostId :: RemoteHostId, @@ -668,14 +670,18 @@ data RemoteHostInfo = RemoteHostInfo displayName :: Text, sessionActive :: Bool } - deriving (Show, Generic, ToJSON) + deriving (Show, Generic, FromJSON) + +instance ToJSON RemoteHostInfo where toEncoding = J.genericToEncoding J.defaultOptions data RemoteCtrlInfo = RemoteCtrlInfo { remoteCtrlId :: RemoteCtrlId, displayName :: Text, sessionActive :: Bool } - deriving (Eq, Show, Generic, ToJSON) + deriving (Eq, Show, Generic, FromJSON) + +instance ToJSON RemoteCtrlInfo where toEncoding = J.genericToEncoding J.defaultOptions newtype UserPwd = UserPwd {unUserPwd :: Text} deriving (Eq, Show) @@ -695,6 +701,9 @@ instance StrEncoding AgentQueueId where strDecode s = AgentQueueId <$> strDecode s strP = AgentQueueId <$> strP +instance FromJSON AgentQueueId where + parseJSON = strParseJSON "AgentQueueId" + instance ToJSON AgentQueueId where toJSON = strToJSON toEncoding = strToJEncoding @@ -713,12 +722,23 @@ data UserProtoServers p = UserProtoServers } deriving (Show, Generic) +instance ProtocolTypeI p => FromJSON (UserProtoServers p) where + parseJSON = J.genericParseJSON J.defaultOptions + instance ProtocolTypeI p => ToJSON (UserProtoServers p) where - toJSON = J.genericToJSON J.defaultOptions toEncoding = J.genericToEncoding J.defaultOptions data AUserProtoServers = forall p. (ProtocolTypeI p, UserProtocol p) => AUPS (UserProtoServers p) +instance FromJSON AUserProtoServers where + parseJSON v = J.withObject "AUserProtoServers" parse v + where + parse o = do + AProtocolType (p :: SProtocolType p) <- o .: "serverProtocol" + case userProtocol p of + Just Dict -> AUPS <$> J.parseJSON @(UserProtoServers p) v + Nothing -> fail $ "AUserProtoServers: unsupported protocol " <> show p + instance ToJSON AUserProtoServers where toJSON (AUPS s) = J.genericToJSON J.defaultOptions s toEncoding (AUPS s) = J.genericToEncoding J.defaultOptions s @@ -747,7 +767,7 @@ data ContactSubStatus = ContactSubStatus { contact :: Contact, contactError :: Maybe ChatError } - deriving (Show, Generic) + deriving (Show, Generic, FromJSON) instance ToJSON ContactSubStatus where toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} @@ -757,7 +777,7 @@ data MemberSubStatus = MemberSubStatus { member :: GroupMember, memberError :: Maybe ChatError } - deriving (Show, Generic) + deriving (Show, Generic, FromJSON) instance ToJSON MemberSubStatus where toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} @@ -767,7 +787,7 @@ data UserContactSubStatus = UserContactSubStatus { userContact :: UserContact, userContactError :: Maybe ChatError } - deriving (Show, Generic) + deriving (Show, Generic, FromJSON) instance ToJSON UserContactSubStatus where toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} @@ -777,7 +797,7 @@ data PendingSubStatus = PendingSubStatus { connection :: PendingContactConnection, connError :: Maybe ChatError } - deriving (Show, Generic) + deriving (Show, Generic, FromJSON) instance ToJSON PendingSubStatus where toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} @@ -789,7 +809,7 @@ data UserProfileUpdateSummary = UserProfileUpdateSummary updateFailures :: Int, changedContacts :: [Contact] } - deriving (Show, Generic) + deriving (Show, Generic, FromJSON) instance ToJSON UserProfileUpdateSummary where toEncoding = J.genericToEncoding J.defaultOptions @@ -825,12 +845,10 @@ data XFTPFileConfig = XFTPFileConfig defaultXFTPFileConfig :: XFTPFileConfig defaultXFTPFileConfig = XFTPFileConfig {minFileSize = 0} -instance ToJSON XFTPFileConfig where - toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} - toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} +instance ToJSON XFTPFileConfig where toEncoding = J.genericToEncoding J.defaultOptions data NtfMsgInfo = NtfMsgInfo {msgTs :: UTCTime, msgFlags :: MsgFlags} - deriving (Show, Generic) + deriving (Show, Generic, FromJSON) instance ToJSON NtfMsgInfo where toEncoding = J.genericToEncoding J.defaultOptions @@ -842,7 +860,7 @@ data SwitchProgress = SwitchProgress switchPhase :: SwitchPhase, connectionStats :: ConnectionStats } - deriving (Show, Generic) + deriving (Show, Generic, FromJSON) instance ToJSON SwitchProgress where toEncoding = J.genericToEncoding J.defaultOptions @@ -850,7 +868,7 @@ data RatchetSyncProgress = RatchetSyncProgress { ratchetSyncStatus :: RatchetSyncState, connectionStats :: ConnectionStats } - deriving (Show, Generic) + deriving (Show, Generic, FromJSON) instance ToJSON RatchetSyncProgress where toEncoding = J.genericToEncoding J.defaultOptions @@ -858,7 +876,7 @@ data ParsedServerAddress = ParsedServerAddress { serverAddress :: Maybe ServerAddress, parseError :: String } - deriving (Show, Generic) + deriving (Show, Generic, FromJSON) instance ToJSON ParsedServerAddress where toEncoding = J.genericToEncoding J.defaultOptions @@ -869,7 +887,7 @@ data ServerAddress = ServerAddress keyHash :: String, basicAuth :: String } - deriving (Show, Generic) + deriving (Show, Generic, FromJSON) instance ToJSON ServerAddress where toEncoding = J.genericToEncoding J.defaultOptions @@ -893,7 +911,7 @@ data CoreVersionInfo = CoreVersionInfo simplexmqVersion :: String, simplexmqCommit :: String } - deriving (Show, Generic) + deriving (Show, Generic, FromJSON) instance ToJSON CoreVersionInfo where toEncoding = J.genericToEncoding J.defaultOptions @@ -906,7 +924,7 @@ data SlowSQLQuery = SlowSQLQuery { query :: Text, queryStats :: SlowQueryStats } - deriving (Show, Generic) + deriving (Show, Generic, FromJSON) instance ToJSON SlowSQLQuery where toEncoding = J.genericToEncoding J.defaultOptions @@ -919,6 +937,9 @@ data ChatError | ChatErrorRemoteHost {remoteHostId :: RemoteHostId, remoteHostError :: RemoteHostError} deriving (Show, Exception, Generic) +instance FromJSON ChatError where + parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "Chat" + instance ToJSON ChatError where toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "Chat" toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "Chat" @@ -1002,6 +1023,9 @@ data ChatErrorType | CEException {message :: String} deriving (Show, Exception, Generic) +instance FromJSON ChatErrorType where + parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "CE" + instance ToJSON ChatErrorType where toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "CE" toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "CE" @@ -1014,6 +1038,9 @@ data DatabaseError | DBErrorOpen {sqliteError :: SQLiteError} deriving (Show, Exception, Generic) +instance FromJSON DatabaseError where + parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "DB" + instance ToJSON DatabaseError where toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "DB" toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "DB" @@ -1021,6 +1048,9 @@ instance ToJSON DatabaseError where data SQLiteError = SQLiteErrorNotADatabase | SQLiteError String deriving (Show, Exception, Generic) +instance FromJSON SQLiteError where + parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "SQLite" + instance ToJSON SQLiteError where toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "SQLite" toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "SQLite" @@ -1070,6 +1100,9 @@ data ArchiveError | AEImportFile {file :: String, chatError :: ChatError} deriving (Show, Exception, Generic) +instance FromJSON ArchiveError where + parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "AE" + instance ToJSON ArchiveError where toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "AE" toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "AE" @@ -1156,3 +1189,5 @@ withStoreCtx ctx_ action = do where handleInternal :: String -> SomeException -> IO (Either StoreError a) handleInternal ctxStr e = pure . Left . SEInternalError $ show e <> ctxStr + +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CR") ''ChatResponse) diff --git a/src/Simplex/Chat/Markdown.hs b/src/Simplex/Chat/Markdown.hs index d18f28db31..64b1145539 100644 --- a/src/Simplex/Chat/Markdown.hs +++ b/src/Simplex/Chat/Markdown.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} @@ -10,7 +11,7 @@ module Simplex.Chat.Markdown where import Control.Applicative (optional, (<|>)) -import Data.Aeson (ToJSON) +import Data.Aeson (FromJSON, ToJSON) import qualified Data.Aeson as J import Data.Attoparsec.Text (Parser) import qualified Data.Attoparsec.Text as A @@ -56,6 +57,9 @@ data Format data SimplexLinkType = XLContact | XLInvitation | XLGroup deriving (Eq, Show, Generic) +instance FromJSON SimplexLinkType where + parseJSON = J.genericParseJSON . enumJSON $ dropPrefix "XL" + instance ToJSON SimplexLinkType where toJSON = J.genericToJSON . enumJSON $ dropPrefix "XL" toEncoding = J.genericToEncoding . enumJSON $ dropPrefix "XL" @@ -66,6 +70,9 @@ colored = Colored . FormatColor markdown :: Format -> Text -> Markdown markdown = Markdown . Just +instance FromJSON Format where + parseJSON = J.genericParseJSON $ sumTypeJSON fstToLower + instance ToJSON Format where toJSON = J.genericToJSON $ sumTypeJSON fstToLower toEncoding = J.genericToEncoding $ sumTypeJSON fstToLower @@ -91,6 +98,18 @@ instance IsString Markdown where fromString = unmarked . T.pack newtype FormatColor = FormatColor Color deriving (Eq, Show) +instance FromJSON FormatColor where + parseJSON = J.withText "FormatColor" $ fmap FormatColor . \case + "red" -> pure Red + "green" -> pure Green + "blue" -> pure Blue + "yellow" -> pure Yellow + "cyan" -> pure Cyan + "magenta" -> pure Magenta + "black" -> pure Black + "white" -> pure White + unexpected -> fail $ "unexpected FormatColor: " <> show unexpected + instance ToJSON FormatColor where toJSON (FormatColor c) = case c of Red -> "red" @@ -103,7 +122,7 @@ instance ToJSON FormatColor where White -> "white" data FormattedText = FormattedText {format :: Maybe Format, text :: Text} - deriving (Eq, Show, Generic) + deriving (Eq, Show, Generic, FromJSON) instance ToJSON FormattedText where toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} @@ -129,7 +148,7 @@ parseMaybeMarkdownList s | otherwise = Just . reverse $ foldl' acc [] ml where ml = intercalate ["\n"] . map (markdownToList . parseMarkdown) $ T.lines s - acc [] m = [m] + acc [] m = [m] acc ms@(FormattedText f t : ms') ft@(FormattedText f' t') | f == f' = FormattedText f (t <> t') : ms' | otherwise = ft : ms diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index 79463d2107..b9ce953731 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -17,7 +17,7 @@ module Simplex.Chat.Messages where import Control.Applicative ((<|>)) -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.Attoparsec.ByteString.Char8 as A @@ -66,6 +66,9 @@ chatNameStr (ChatName cType name) = chatTypeStr cType <> T.unpack name data ChatRef = ChatRef ChatType Int64 deriving (Eq, Show, Ord) +instance FromJSON ChatType where + parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "CT" + instance ToJSON ChatType where toJSON = J.genericToJSON . enumJSON $ dropPrefix "CT" toEncoding = J.genericToEncoding . enumJSON $ dropPrefix "CT" @@ -110,10 +113,16 @@ data JSONChatInfo | JCInfoContactConnection {contactConnection :: PendingContactConnection} deriving (Generic) +instance FromJSON JSONChatInfo where + parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "JCInfo" + instance ToJSON JSONChatInfo where toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "JCInfo" toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "JCInfo" +instance ChatTypeI c => FromJSON (ChatInfo c) where + parseJSON v = (\(AChatInfo _ c) -> checkChatType c) <$?> J.parseJSON v + instance ToJSON (ChatInfo c) where toJSON = J.toJSON . jsonChatInfo toEncoding = J.toEncoding . jsonChatInfo @@ -125,10 +134,20 @@ jsonChatInfo = \case ContactRequest g -> JCInfoContactRequest g ContactConnection c -> JCInfoContactConnection c -data AChatInfo = forall c. AChatInfo (SChatType c) (ChatInfo c) +data AChatInfo = forall c. ChatTypeI c => AChatInfo (SChatType c) (ChatInfo c) deriving instance Show AChatInfo +jsonAChatInfo :: JSONChatInfo -> AChatInfo +jsonAChatInfo = \case + JCInfoDirect c -> AChatInfo SCTDirect $ DirectChat c + JCInfoGroup g -> AChatInfo SCTGroup $ GroupChat g + JCInfoContactRequest g -> AChatInfo SCTContactRequest $ ContactRequest g + JCInfoContactConnection c -> AChatInfo SCTContactConnection $ ContactConnection c + +instance FromJSON AChatInfo where + parseJSON v = jsonAChatInfo <$> J.parseJSON v + instance ToJSON AChatInfo where toJSON (AChatInfo _ c) = J.toJSON c toEncoding (AChatInfo _ c) = J.toEncoding c @@ -144,7 +163,10 @@ data ChatItem (c :: ChatType) (d :: MsgDirection) = ChatItem } deriving (Show, Generic) -instance MsgDirectionI d => ToJSON (ChatItem c d) where +instance (ChatTypeI c, MsgDirectionI d) => FromJSON (ChatItem c d) where + parseJSON = J.genericParseJSON J.defaultOptions + +instance (ChatTypeI c, MsgDirectionI d) => ToJSON (ChatItem c d) where toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} @@ -156,6 +178,16 @@ data CIDirection (c :: ChatType) (d :: MsgDirection) where deriving instance Show (CIDirection c d) +data CCIDirection c = forall d. MsgDirectionI d => CCID (SMsgDirection d) (CIDirection c d) + +instance ChatTypeI c => FromJSON (CCIDirection c) where + parseJSON v = (\(ACID _ d x) -> checkChatType (CCID d x)) <$?> J.parseJSON v + +data ACIDirection = forall c d. (ChatTypeI c, MsgDirectionI d) => ACID (SChatType c) (SMsgDirection d) (CIDirection c d) + +instance FromJSON ACIDirection where + parseJSON v = jsonACIDirection <$> J.parseJSON v + data JSONCIDirection = JCIDirectSnd | JCIDirectRcv @@ -163,10 +195,16 @@ data JSONCIDirection | JCIGroupRcv {groupMember :: GroupMember} deriving (Generic, Show) +instance FromJSON JSONCIDirection where + parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "JCI" + instance ToJSON JSONCIDirection where toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "JCI" toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "JCI" +instance (ChatTypeI c, MsgDirectionI d) => FromJSON (CIDirection c d) where + parseJSON v = (\(CCID _ x') -> checkDirection x') <$?> J.parseJSON v + instance ToJSON (CIDirection c d) where toJSON = J.toJSON . jsonCIDirection toEncoding = J.toEncoding . jsonCIDirection @@ -178,8 +216,15 @@ jsonCIDirection = \case CIGroupSnd -> JCIGroupSnd CIGroupRcv m -> JCIGroupRcv m +jsonACIDirection :: JSONCIDirection -> ACIDirection +jsonACIDirection = \case + JCIDirectSnd -> ACID SCTDirect SMDSnd CIDirectSnd + JCIDirectRcv -> ACID SCTDirect SMDRcv CIDirectRcv + JCIGroupSnd -> ACID SCTGroup SMDSnd CIGroupSnd + JCIGroupRcv m -> ACID SCTGroup SMDRcv $ CIGroupRcv m + data CIReactionCount = CIReactionCount {reaction :: MsgReaction, userReacted :: Bool, totalReacted :: Int} - deriving (Show, Generic) + deriving (Show, Generic, FromJSON) instance ToJSON CIReactionCount where toEncoding = J.genericToEncoding J.defaultOptions @@ -187,7 +232,15 @@ data CChatItem c = forall d. MsgDirectionI d => CChatItem (SMsgDirection d) (Cha deriving instance Show (CChatItem c) -instance ToJSON (CChatItem c) where +instance forall c. ChatTypeI c => FromJSON (CChatItem c) where + parseJSON v = J.withObject "CChatItem" parse v + where + parse o = do + CCID d (_ :: CIDirection c d) <- o .: "chatDir" + ci <- J.parseJSON @(ChatItem c d) v + pure $ CChatItem d ci + +instance ChatTypeI c => ToJSON (CChatItem c) where toJSON (CChatItem _ ci) = J.toJSON ci toEncoding (CChatItem _ ci) = J.toEncoding ci @@ -279,14 +332,19 @@ data Chat c = Chat } deriving (Show, Generic) -instance ToJSON (Chat c) where - toJSON = J.genericToJSON J.defaultOptions - toEncoding = J.genericToEncoding J.defaultOptions +instance ChatTypeI c => ToJSON (Chat c) where toEncoding = J.genericToEncoding J.defaultOptions -data AChat = forall c. AChat (SChatType c) (Chat c) +data AChat = forall c. ChatTypeI c => AChat (SChatType c) (Chat c) deriving instance Show AChat +instance FromJSON AChat where + parseJSON = J.withObject "AChat" $ \o -> do + AChatInfo c chatInfo <- o .: "chatInfo" + chatItems <- o .: "chatItems" + chatStats <- o .: "chatStats" + pure $ AChat c Chat {chatInfo, chatItems, chatStats} + instance ToJSON AChat where toJSON (AChat _ c) = J.toJSON c toEncoding (AChat _ c) = J.toEncoding c @@ -296,17 +354,21 @@ data ChatStats = ChatStats minUnreadItemId :: ChatItemId, unreadChat :: Bool } - deriving (Show, Generic) + deriving (Show, Generic, FromJSON) -instance ToJSON ChatStats where - toJSON = J.genericToJSON J.defaultOptions - toEncoding = J.genericToEncoding J.defaultOptions +instance ToJSON ChatStats where toEncoding = J.genericToEncoding J.defaultOptions -- | type to show a mix of messages from multiple chats -data AChatItem = forall c d. MsgDirectionI d => AChatItem (SChatType c) (SMsgDirection d) (ChatInfo c) (ChatItem c d) +data AChatItem = forall c d. (ChatTypeI c, MsgDirectionI d) => AChatItem (SChatType c) (SMsgDirection d) (ChatInfo c) (ChatItem c d) deriving instance Show AChatItem +instance FromJSON AChatItem where + parseJSON = J.withObject "AChatItem" $ \o -> do + AChatInfo c chatInfo <- o .: "chatInfo" + CChatItem d chatItem <- o .: "chatItem" + pure $ AChatItem c d chatInfo chatItem + instance ToJSON AChatItem where toJSON (AChatItem _ _ chat item) = J.toJSON $ JSONAnyChatItem chat item toEncoding (AChatItem _ _ chat item) = J.toEncoding $ JSONAnyChatItem chat item @@ -330,7 +392,7 @@ updateFileStatus ci@ChatItem {file} status = case file of Just f -> ci {file = Just (f :: CIFile d) {fileStatus = status}} Nothing -> ci -instance MsgDirectionI d => ToJSON (JSONAnyChatItem c d) where +instance (ChatTypeI c, MsgDirectionI d) => ToJSON (JSONAnyChatItem c d) where toJSON = J.genericToJSON J.defaultOptions toEncoding = J.genericToEncoding J.defaultOptions @@ -349,7 +411,7 @@ data CIMeta (c :: ChatType) (d :: MsgDirection) = CIMeta createdAt :: UTCTime, updatedAt :: UTCTime } - deriving (Show, Generic) + deriving (Show, Generic, FromJSON) mkCIMeta :: ChatItemId -> CIContent d -> Text -> CIStatus d -> Maybe SharedMsgId -> Maybe (CIDeleted c) -> Bool -> Maybe CITimed -> Maybe Bool -> UTCTime -> ChatItemTs -> UTCTime -> UTCTime -> CIMeta c d mkCIMeta itemId itemContent itemText itemStatus itemSharedMsgId itemDeleted itemEdited itemTimed itemLive currentTs itemTs createdAt updatedAt = @@ -358,13 +420,13 @@ mkCIMeta itemId itemContent itemText itemStatus itemSharedMsgId itemDeleted item _ -> False in CIMeta {itemId, itemTs, itemText, itemStatus, itemSharedMsgId, itemDeleted, itemEdited, itemTimed, itemLive, editable, createdAt, updatedAt} -instance ToJSON (CIMeta c d) where toEncoding = J.genericToEncoding J.defaultOptions +instance ChatTypeI c => ToJSON (CIMeta c d) where toEncoding = J.genericToEncoding J.defaultOptions data CITimed = CITimed { ttl :: Int, -- seconds deleteAt :: Maybe UTCTime -- this is initially Nothing for received items, the timer starts when they are read } - deriving (Show, Generic) + deriving (Show, Generic, FromJSON) instance ToJSON CITimed where toEncoding = J.genericToEncoding J.defaultOptions @@ -402,6 +464,9 @@ data CIQuote (c :: ChatType) = CIQuote } deriving (Show, Generic) +instance ChatTypeI c => FromJSON (CIQuote c) where + parseJSON = J.genericParseJSON J.defaultOptions + instance ToJSON (CIQuote c) where toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} @@ -414,24 +479,39 @@ data CIReaction (c :: ChatType) (d :: MsgDirection) = CIReaction } deriving (Show, Generic) -instance ToJSON (CIReaction c d) where - toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} - toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} +instance (ChatTypeI c, MsgDirectionI d) => FromJSON (CIReaction c d) where + parseJSON = J.genericParseJSON J.defaultOptions -data ACIReaction = forall c d. ACIReaction (SChatType c) (SMsgDirection d) (ChatInfo c) (CIReaction c d) +instance ChatTypeI c => ToJSON (CIReaction c d) where + toEncoding = J.genericToEncoding J.defaultOptions + +data AnyCIReaction = forall c d. ChatTypeI c => ACIR (SChatType c) (SMsgDirection d) (CIReaction c d) + +instance FromJSON AnyCIReaction where + parseJSON v = J.withObject "AnyCIReaction" parse v + where + parse o = do + ACID c d (_ :: CIDirection c d) <- o .: "chatDir" + ACIR c d <$> J.parseJSON @(CIReaction c d) v + +data ACIReaction = forall c d. ChatTypeI c => ACIReaction (SChatType c) (SMsgDirection d) (ChatInfo c) (CIReaction c d) deriving instance Show ACIReaction +instance FromJSON ACIReaction where + parseJSON = J.withObject "ACIReaction" $ \o -> do + ACIR c d reaction <- o .: "chatReaction" + cInfo <- o .: "chatInfo" + pure $ ACIReaction c d cInfo reaction + instance ToJSON ACIReaction where - toJSON (ACIReaction _ _ chat reaction) = J.toJSON $ JSONCIReaction chat reaction - toEncoding (ACIReaction _ _ chat reaction) = J.toEncoding $ JSONCIReaction chat reaction + toJSON (ACIReaction _ _ cInfo reaction) = J.toJSON $ JSONCIReaction cInfo reaction + toEncoding (ACIReaction _ _ cInfo reaction) = J.toEncoding $ JSONCIReaction cInfo reaction data JSONCIReaction c d = JSONCIReaction {chatInfo :: ChatInfo c, chatReaction :: CIReaction c d} deriving (Generic) -instance ToJSON (JSONCIReaction c d) where - toJSON = J.genericToJSON J.defaultOptions - toEncoding = J.genericToEncoding J.defaultOptions +instance ChatTypeI c => ToJSON (JSONCIReaction c d) where toEncoding = J.genericToEncoding J.defaultOptions data CIQDirection (c :: ChatType) where CIQDirectSnd :: CIQDirection 'CTDirect @@ -441,6 +521,11 @@ data CIQDirection (c :: ChatType) where deriving instance Show (CIQDirection c) +data ACIQDirection = forall c. ChatTypeI c => ACIQDirection (SChatType c) (CIQDirection c) + +instance ChatTypeI c => FromJSON (CIQDirection c) where + parseJSON v = (\(ACIQDirection _ x) -> checkChatType x) . jsonACIQDirection <$?> J.parseJSON v + instance ToJSON (CIQDirection c) where toJSON = J.toJSON . jsonCIQDirection toEncoding = J.toEncoding . jsonCIQDirection @@ -453,6 +538,14 @@ jsonCIQDirection = \case CIQGroupRcv (Just m) -> Just $ JCIGroupRcv m CIQGroupRcv Nothing -> Nothing +jsonACIQDirection :: Maybe JSONCIDirection -> ACIQDirection +jsonACIQDirection = \case + Just JCIDirectSnd -> ACIQDirection SCTDirect CIQDirectSnd + Just JCIDirectRcv -> ACIQDirection SCTDirect CIQDirectRcv + Just JCIGroupSnd -> ACIQDirection SCTGroup CIQGroupSnd + Just (JCIGroupRcv m) -> ACIQDirection SCTGroup $ CIQGroupRcv (Just m) + Nothing -> ACIQDirection SCTGroup $ CIQGroupRcv Nothing + quoteMsgDirection :: CIQDirection c -> MsgDirection quoteMsgDirection = \case CIQDirectSnd -> MDSnd @@ -470,6 +563,9 @@ data CIFile (d :: MsgDirection) = CIFile } deriving (Show, Generic) +instance MsgDirectionI d => FromJSON (CIFile d) where + parseJSON = J.genericParseJSON J.defaultOptions + instance MsgDirectionI d => ToJSON (CIFile d) where toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} @@ -481,6 +577,9 @@ instance FromField FileProtocol where fromField = fromTextField_ textDecode instance ToField FileProtocol where toField = toField . textEncode +instance FromJSON FileProtocol where + parseJSON = textParseJSON "FileProtocol" + instance ToJSON FileProtocol where toJSON = J.String . textEncode toEncoding = JE.text . textEncode @@ -527,6 +626,9 @@ ciFileEnded = \case CIFSRcvError -> True CIFSInvalid {} -> True +instance MsgDirectionI d => FromJSON (CIFileStatus d) where + parseJSON v = (\(AFS _ s) -> checkDirection s) . aciFileStatusJSON <$?> J.parseJSON v + instance ToJSON (CIFileStatus d) where toJSON = J.toJSON . jsonCIFileStatus toEncoding = J.toEncoding . jsonCIFileStatus @@ -594,6 +696,9 @@ data JSONCIFileStatus | JCIFSInvalid {text :: Text} deriving (Generic) +instance FromJSON JSONCIFileStatus where + parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "JCIFS" + instance ToJSON JSONCIFileStatus where toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "JCIFS" toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "JCIFS" @@ -658,6 +763,9 @@ deriving instance Eq (CIStatus d) deriving instance Show (CIStatus d) +instance MsgDirectionI d => FromJSON (CIStatus d) where + parseJSON v = (\(ACIStatus _ s) -> checkDirection s) . jsonACIStatus <$?> J.parseJSON v + instance ToJSON (CIStatus d) where toJSON = J.toJSON . jsonCIStatus toEncoding = J.toEncoding . jsonCIStatus @@ -712,6 +820,9 @@ data JSONCIStatus | JCISInvalid {text :: Text} deriving (Show, Generic) +instance FromJSON JSONCIStatus where + parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "JCIS" + instance ToJSON JSONCIStatus where toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "JCIS" toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "JCIS" @@ -727,6 +838,17 @@ jsonCIStatus = \case CISRcvRead -> JCISRcvRead CISInvalid text -> JCISInvalid text +jsonACIStatus :: JSONCIStatus -> ACIStatus +jsonACIStatus = \case + JCISSndNew -> ACIStatus SMDSnd CISSndNew + JCISSndSent sndProgress -> ACIStatus SMDSnd $ CISSndSent sndProgress + JCISSndRcvd msgRcptStatus sndProgress -> ACIStatus SMDSnd $ CISSndRcvd msgRcptStatus sndProgress + JCISSndErrorAuth -> ACIStatus SMDSnd CISSndErrorAuth + JCISSndError e -> ACIStatus SMDSnd $ CISSndError e + JCISRcvNew -> ACIStatus SMDRcv CISRcvNew + JCISRcvRead -> ACIStatus SMDRcv CISRcvRead + JCISInvalid text -> ACIStatus SMDSnd $ CISInvalid text + ciStatusNew :: forall d. MsgDirectionI d => CIStatus d ciStatusNew = case msgDirection @d of SMDSnd -> CISSndNew @@ -757,6 +879,9 @@ data SndCIStatusProgress | SSPComplete deriving (Eq, Show, Generic) +instance FromJSON SndCIStatusProgress where + parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "SSP" + instance ToJSON SndCIStatusProgress where toJSON = J.genericToJSON . enumJSON $ dropPrefix "SSP" toEncoding = J.genericToEncoding . enumJSON $ dropPrefix "SSP" @@ -796,6 +921,8 @@ instance TestEquality SChatType where testEquality SCTContactConnection SCTContactConnection = Just Refl testEquality _ _ = Nothing +data AChatType = forall c. ChatTypeI c => ACT (SChatType c) + class ChatTypeI (c :: ChatType) where chatTypeI :: SChatType c @@ -803,6 +930,36 @@ instance ChatTypeI 'CTDirect where chatTypeI = SCTDirect instance ChatTypeI 'CTGroup where chatTypeI = SCTGroup +instance ChatTypeI 'CTContactRequest where chatTypeI = SCTContactRequest + +instance ChatTypeI 'CTContactConnection where chatTypeI = SCTContactConnection + +instance ChatTypeI c => FromJSON (SChatType c) where + parseJSON v = (\(ACT t) -> checkChatType t) . aChatType <$?> J.parseJSON v + +instance ToJSON (SChatType c) where + toJSON = J.toJSON . toChatType + toEncoding = J.toEncoding . toChatType + +toChatType :: SChatType c -> ChatType +toChatType = \case + SCTDirect -> CTDirect + SCTGroup -> CTGroup + SCTContactRequest -> CTContactRequest + SCTContactConnection -> CTContactConnection + +aChatType :: ChatType -> AChatType +aChatType = \case + CTDirect -> ACT SCTDirect + CTGroup -> ACT SCTGroup + CTContactRequest -> ACT SCTContactRequest + CTContactConnection -> ACT SCTContactConnection + +checkChatType :: forall t c c'. (ChatTypeI c, ChatTypeI c') => t c' -> Either String (t c) +checkChatType x = case testEquality (chatTypeI @c) (chatTypeI @c') of + Just Refl -> Right x + Nothing -> Left "bad chat type" + data NewMessage e = NewMessage { chatMsgEvent :: ChatMsgEvent e, msgBody :: MsgBody @@ -920,35 +1077,43 @@ msgDeliveryStatusT' s = Just Refl -> Just st _ -> Nothing -checkDirection :: forall t d d'. (MsgDirectionI d, MsgDirectionI d') => t d' -> Either String (t d) -checkDirection x = case testEquality (msgDirection @d) (msgDirection @d') of - Just Refl -> Right x - Nothing -> Left "bad direction" - data CIDeleted (c :: ChatType) where CIDeleted :: Maybe UTCTime -> CIDeleted c CIModerated :: Maybe UTCTime -> GroupMember -> CIDeleted 'CTGroup deriving instance Show (CIDeleted c) -instance ToJSON (CIDeleted d) where +data ACIDeleted = forall c. ChatTypeI c => ACIDeleted (SChatType c) (CIDeleted c) + +instance ChatTypeI c => FromJSON (CIDeleted c) where + parseJSON v = (\(ACIDeleted _ x) -> checkChatType x) . jsonACIDeleted <$?> J.parseJSON v + +instance ChatTypeI c => ToJSON (CIDeleted c) where toJSON = J.toJSON . jsonCIDeleted toEncoding = J.toEncoding . jsonCIDeleted data JSONCIDeleted - = JCIDDeleted {deletedTs :: Maybe UTCTime} + = JCIDDeleted {deletedTs :: Maybe UTCTime, chatType :: ChatType} | JCIDModerated {deletedTs :: Maybe UTCTime, byGroupMember :: GroupMember} deriving (Show, Generic) +instance FromJSON JSONCIDeleted where + parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "JCID" + instance ToJSON JSONCIDeleted where toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "JCID" toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "JCID" -jsonCIDeleted :: CIDeleted d -> JSONCIDeleted +jsonCIDeleted :: forall d. ChatTypeI d => CIDeleted d -> JSONCIDeleted jsonCIDeleted = \case - CIDeleted ts -> JCIDDeleted ts + CIDeleted ts -> JCIDDeleted ts (toChatType $ chatTypeI @d) CIModerated ts m -> JCIDModerated ts m +jsonACIDeleted :: JSONCIDeleted -> ACIDeleted +jsonACIDeleted = \case + JCIDDeleted ts cType -> case aChatType cType of ACT c -> ACIDeleted c $ CIDeleted ts + JCIDModerated ts m -> ACIDeleted SCTGroup (CIModerated ts m) + itemDeletedTs :: CIDeleted d -> Maybe UTCTime itemDeletedTs = \case CIDeleted ts -> ts @@ -958,7 +1123,7 @@ data ChatItemInfo = ChatItemInfo { itemVersions :: [ChatItemVersion], memberDeliveryStatuses :: Maybe [MemberDeliveryStatus] } - deriving (Eq, Show, Generic) + deriving (Eq, Show, Generic, FromJSON) instance ToJSON ChatItemInfo where toEncoding = J.genericToEncoding J.defaultOptions @@ -969,7 +1134,7 @@ data ChatItemVersion = ChatItemVersion itemVersionTs :: UTCTime, createdAt :: UTCTime } - deriving (Eq, Show, Generic) + deriving (Eq, Show, Generic, FromJSON) instance ToJSON ChatItemVersion where toEncoding = J.genericToEncoding J.defaultOptions @@ -990,7 +1155,7 @@ data MemberDeliveryStatus = MemberDeliveryStatus { groupMemberId :: GroupMemberId, memberDeliveryStatus :: CIStatus 'MDSnd } - deriving (Eq, Show, Generic) + deriving (Eq, Show, Generic, FromJSON) instance ToJSON MemberDeliveryStatus where toEncoding = J.genericToEncoding J.defaultOptions diff --git a/src/Simplex/Chat/Messages/CIContent.hs b/src/Simplex/Chat/Messages/CIContent.hs index 9abc8e4644..8f9a453bde 100644 --- a/src/Simplex/Chat/Messages/CIContent.hs +++ b/src/Simplex/Chat/Messages/CIContent.hs @@ -9,12 +9,14 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeApplications #-} module Simplex.Chat.Messages.CIContent where import Data.Aeson (FromJSON, ToJSON) import qualified Data.Aeson as J +import qualified Data.Aeson.TH as JQ import Data.Int (Int64) import Data.Text (Text) import Data.Text.Encoding (decodeLatin1, encodeUtf8) @@ -34,7 +36,7 @@ import Simplex.Chat.Types.Util import Simplex.Messaging.Agent.Protocol (MsgErrorType (..), RatchetSyncState (..), SwitchPhase (..)) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (dropPrefix, enumJSON, fstToLower, singleFieldJSON, sumTypeJSON) -import Simplex.Messaging.Util (safeDecodeUtf8, tshow) +import Simplex.Messaging.Util (safeDecodeUtf8, tshow, (<$?>)) data MsgDirection = MDRcv | MDSnd deriving (Eq, Show, Generic) @@ -69,6 +71,13 @@ instance TestEquality SMsgDirection where testEquality SMDSnd SMDSnd = Just Refl testEquality _ _ = Nothing +instance MsgDirectionI d => FromJSON (SMsgDirection d) where + parseJSON v = (\(AMsgDirection d) -> checkDirection d) . fromMsgDirection <$?> J.parseJSON v + +instance ToJSON (SMsgDirection d) where + toJSON = J.toJSON . toMsgDirection + toEncoding = J.toEncoding . toMsgDirection + instance ToField (SMsgDirection d) where toField = toField . msgDirectionInt . toMsgDirection data AMsgDirection = forall d. MsgDirectionI d => AMsgDirection (SMsgDirection d) @@ -92,6 +101,11 @@ instance MsgDirectionI 'MDRcv where msgDirection = SMDRcv instance MsgDirectionI 'MDSnd where msgDirection = SMDSnd +checkDirection :: forall t d d'. (MsgDirectionI d, MsgDirectionI d') => t d' -> Either String (t d) +checkDirection x = case testEquality (msgDirection @d) (msgDirection @d') of + Just Refl -> Right x + Nothing -> Left "bad direction" + msgDirectionInt :: MsgDirection -> Int msgDirectionInt = \case MDRcv -> 0 @@ -481,27 +495,10 @@ msgDirToModeratedContent_ = \case ciModeratedText :: Text ciModeratedText = "moderated" --- platform independent -instance MsgDirectionI d => ToField (CIContent d) where - toField = toField . encodeJSON . dbJsonCIContent - --- platform specific -instance MsgDirectionI d => ToJSON (CIContent d) where - toJSON = J.toJSON . jsonCIContent - toEncoding = J.toEncoding . jsonCIContent - data ACIContent = forall d. MsgDirectionI d => ACIContent (SMsgDirection d) (CIContent d) deriving instance Show ACIContent --- platform independent -dbParseACIContent :: Text -> Either String ACIContent -dbParseACIContent = fmap aciContentDBJSON . J.eitherDecodeStrict' . encodeUtf8 - --- platform specific -instance FromJSON ACIContent where - parseJSON = fmap aciContentJSON . J.parseJSON - -- platform specific data JSONCIContent = JCISndMsgContent {msgContent :: MsgContent} @@ -527,17 +524,9 @@ data JSONCIContent | JCISndGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int} | JCIRcvChatFeatureRejected {feature :: ChatFeature} | JCIRcvGroupFeatureRejected {groupFeature :: GroupFeature} - | JCISndModerated - | JCIRcvModerated + | JCISndModerated {_nullary :: Maybe Int} + | JCIRcvModerated {_nullary :: Maybe Int} | JCIInvalidJSON {direction :: MsgDirection, json :: Text} - deriving (Generic) - -instance FromJSON JSONCIContent where - parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "JCI" - -instance ToJSON JSONCIContent where - toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "JCI" - toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "JCI" jsonCIContent :: forall d. MsgDirectionI d => CIContent d -> JSONCIContent jsonCIContent = \case @@ -564,8 +553,8 @@ jsonCIContent = \case CISndGroupFeature groupFeature preference param -> JCISndGroupFeature {groupFeature, preference, param} CIRcvChatFeatureRejected feature -> JCIRcvChatFeatureRejected {feature} CIRcvGroupFeatureRejected groupFeature -> JCIRcvGroupFeatureRejected {groupFeature} - CISndModerated -> JCISndModerated - CIRcvModerated -> JCISndModerated + CISndModerated -> JCISndModerated Nothing + CIRcvModerated -> JCISndModerated Nothing CIInvalidJSON json -> JCIInvalidJSON (toMsgDirection $ msgDirection @d) json aciContentJSON :: JSONCIContent -> ACIContent @@ -593,8 +582,8 @@ aciContentJSON = \case JCISndGroupFeature {groupFeature, preference, param} -> ACIContent SMDSnd $ CISndGroupFeature groupFeature preference param JCIRcvChatFeatureRejected {feature} -> ACIContent SMDRcv $ CIRcvChatFeatureRejected feature JCIRcvGroupFeatureRejected {groupFeature} -> ACIContent SMDRcv $ CIRcvGroupFeatureRejected groupFeature - JCISndModerated -> ACIContent SMDSnd CISndModerated - JCIRcvModerated -> ACIContent SMDRcv CIRcvModerated + JCISndModerated _ -> ACIContent SMDSnd CISndModerated + JCIRcvModerated _ -> ACIContent SMDRcv CIRcvModerated JCIInvalidJSON dir json -> case fromMsgDirection dir of AMsgDirection d -> ACIContent d $ CIInvalidJSON json @@ -623,17 +612,9 @@ data DBJSONCIContent | DBJCISndGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int} | DBJCIRcvChatFeatureRejected {feature :: ChatFeature} | DBJCIRcvGroupFeatureRejected {groupFeature :: GroupFeature} - | DBJCISndModerated - | DBJCIRcvModerated + | DBJCISndModerated {_nullary :: Maybe Int} + | DBJCIRcvModerated {_nullary :: Maybe Int} | DBJCIInvalidJSON {direction :: MsgDirection, json :: Text} - deriving (Generic) - -instance FromJSON DBJSONCIContent where - parseJSON = J.genericParseJSON . singleFieldJSON $ dropPrefix "DBJCI" - -instance ToJSON DBJSONCIContent where - toJSON = J.genericToJSON . singleFieldJSON $ dropPrefix "DBJCI" - toEncoding = J.genericToEncoding . singleFieldJSON $ dropPrefix "DBJCI" dbJsonCIContent :: forall d. MsgDirectionI d => CIContent d -> DBJSONCIContent dbJsonCIContent = \case @@ -660,8 +641,8 @@ dbJsonCIContent = \case CISndGroupFeature groupFeature preference param -> DBJCISndGroupFeature {groupFeature, preference, param} CIRcvChatFeatureRejected feature -> DBJCIRcvChatFeatureRejected {feature} CIRcvGroupFeatureRejected groupFeature -> DBJCIRcvGroupFeatureRejected {groupFeature} - CISndModerated -> DBJCISndModerated - CIRcvModerated -> DBJCIRcvModerated + CISndModerated -> DBJCISndModerated Nothing + CIRcvModerated -> DBJCIRcvModerated Nothing CIInvalidJSON json -> DBJCIInvalidJSON (toMsgDirection $ msgDirection @d) json aciContentDBJSON :: DBJSONCIContent -> ACIContent @@ -689,8 +670,8 @@ aciContentDBJSON = \case DBJCISndGroupFeature {groupFeature, preference, param} -> ACIContent SMDSnd $ CISndGroupFeature groupFeature preference param DBJCIRcvChatFeatureRejected {feature} -> ACIContent SMDRcv $ CIRcvChatFeatureRejected feature DBJCIRcvGroupFeatureRejected {groupFeature} -> ACIContent SMDRcv $ CIRcvGroupFeatureRejected groupFeature - DBJCISndModerated -> ACIContent SMDSnd CISndModerated - DBJCIRcvModerated -> ACIContent SMDRcv CIRcvModerated + DBJCISndModerated _ -> ACIContent SMDSnd CISndModerated + DBJCIRcvModerated _ -> ACIContent SMDRcv CIRcvModerated DBJCIInvalidJSON dir json -> case fromMsgDirection dir of AMsgDirection d -> ACIContent d $ CIInvalidJSON json @@ -703,14 +684,7 @@ data CICallStatus | CISCallProgress | CISCallEnded | CISCallError - deriving (Show, Generic) - -instance FromJSON CICallStatus where - parseJSON = J.genericParseJSON . enumJSON $ dropPrefix "CISCall" - -instance ToJSON CICallStatus where - toJSON = J.genericToJSON . enumJSON $ dropPrefix "CISCall" - toEncoding = J.genericToEncoding . enumJSON $ dropPrefix "CISCall" + deriving (Show) ciCallInfoText :: CICallStatus -> Int -> Text ciCallInfoText status duration = case status of @@ -722,3 +696,31 @@ ciCallInfoText status duration = case status of CISCallProgress -> "in progress " <> durationText duration CISCallEnded -> "ended " <> durationText duration CISCallError -> "error" + +$(JQ.deriveJSON (enumJSON $ dropPrefix "CISCall") ''CICallStatus) + +-- platform specific +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "JCI") ''JSONCIContent) + +-- platform independent +$(JQ.deriveJSON (singleFieldJSON $ dropPrefix "DBJCI") ''DBJSONCIContent) + +-- platform independent +instance MsgDirectionI d => ToField (CIContent d) where + toField = toField . encodeJSON . dbJsonCIContent + +-- platform specific +instance MsgDirectionI d => ToJSON (CIContent d) where + toJSON = J.toJSON . jsonCIContent + toEncoding = J.toEncoding . jsonCIContent + +instance MsgDirectionI d => FromJSON (CIContent d) where + parseJSON v = (\(ACIContent _ c) -> checkDirection c) <$?> J.parseJSON v + +-- platform independent +dbParseACIContent :: Text -> Either String ACIContent +dbParseACIContent = fmap aciContentDBJSON . J.eitherDecodeStrict' . encodeUtf8 + +-- platform specific +instance FromJSON ACIContent where + parseJSON = fmap aciContentJSON . J.parseJSON diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index bbdddf8ce0..cb937441f9 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -72,6 +72,9 @@ data ConnectionEntity | UserContactConnection {entityConnection :: Connection, userContact :: UserContact} deriving (Eq, Show, Generic) +instance FromJSON ConnectionEntity where + parseJSON = J.genericParseJSON $ sumTypeJSON fstToLower + instance ToJSON ConnectionEntity where toJSON = J.genericToJSON $ sumTypeJSON fstToLower toEncoding = J.genericToEncoding $ sumTypeJSON fstToLower diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index 936c750c6c..8f7a3b4f4c 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -201,7 +201,7 @@ startRemoteCtrl = chatWriteVar remoteCtrlSession Nothing toView $ CRRemoteCtrlStopped {remoteCtrlId} chatWriteVar remoteCtrlSession $ Just RemoteCtrlSession {discoverer, supervisor, hostServer = Nothing, discovered, accepted} - pure CRRemoteCtrlStarted + pure $ CRRemoteCtrlStarted Nothing discoverRemoteCtrls :: (ChatMonad m) => TM.TMap C.KeyHash TransportHost -> m () discoverRemoteCtrls discovered = Discovery.openListener >>= go diff --git a/src/Simplex/Chat/Remote/Types.hs b/src/Simplex/Chat/Remote/Types.hs index b66e9a6253..f13c3c84ea 100644 --- a/src/Simplex/Chat/Remote/Types.hs +++ b/src/Simplex/Chat/Remote/Types.hs @@ -1,14 +1,14 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE TemplateHaskell #-} module Simplex.Chat.Remote.Types where import Control.Concurrent.Async (Async) -import Data.Aeson (ToJSON (..)) +import qualified Data.Aeson.TH as J import Data.Int (Int64) import Data.Text (Text) -import GHC.Generics (Generic) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.TMap (TMap) import Simplex.Messaging.Transport.Client (TransportHost) @@ -37,7 +37,9 @@ data RemoteCtrl = RemoteCtrl fingerprint :: C.KeyHash, accepted :: Maybe Bool } - deriving (Show, Generic, ToJSON) + deriving (Show) + +$(J.deriveJSON J.defaultOptions ''RemoteCtrl) data RemoteHostSession = RemoteHostSessionStarting diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index e521cb43cf..d005c3893e 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE NamedFieldPuns #-} @@ -59,7 +60,7 @@ where import Control.Monad import Control.Monad.Except import Control.Monad.IO.Class -import Data.Aeson (ToJSON) +import Data.Aeson (FromJSON, ToJSON) import qualified Data.Aeson as J import Data.Functor (($>)) import Data.Int (Int64) @@ -398,7 +399,7 @@ data UserContactLink = UserContactLink { connReqContact :: ConnReqContact, autoAccept :: Maybe AutoAccept } - deriving (Show, Generic) + deriving (Show, Generic, FromJSON) instance ToJSON UserContactLink where toEncoding = J.genericToEncoding J.defaultOptions @@ -406,7 +407,7 @@ data AutoAccept = AutoAccept { acceptIncognito :: IncognitoEnabled, autoReply :: Maybe MsgContent } - deriving (Show, Generic) + deriving (Show, Generic, FromJSON) instance ToJSON AutoAccept where toEncoding = J.genericToEncoding J.defaultOptions diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index 4dc4f6e82d..64634dd29d 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -16,7 +16,7 @@ import Control.Monad import Control.Monad.Except import Control.Monad.IO.Class import Crypto.Random (ChaChaDRG, randomBytesGenerate) -import Data.Aeson (ToJSON) +import Data.Aeson (FromJSON, ToJSON) import qualified Data.Aeson as J import qualified Data.ByteString.Base64 as B64 import Data.ByteString.Char8 (ByteString) @@ -102,6 +102,9 @@ data StoreError | SENoGroupSndStatus {itemId :: ChatItemId, groupMemberId :: GroupMemberId} deriving (Show, Exception, Generic) +instance FromJSON StoreError where + parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "SE" + instance ToJSON StoreError where toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "SE" toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "SE" diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 0b143a7574..088f23e056 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -15,6 +15,7 @@ {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StrictData #-} +{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilyDependencies #-} {-# LANGUAGE UndecidableInstances #-} @@ -25,9 +26,10 @@ module Simplex.Chat.Types where import Crypto.Number.Serialize (os2ip) -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.TH as JQ import qualified Data.Aeson.Types as JT import qualified Data.Attoparsec.ByteString.Char8 as A import Data.ByteString.Char8 (ByteString, pack, unpack) @@ -112,18 +114,14 @@ data User = User sendRcptsContacts :: Bool, sendRcptsSmallGroups :: Bool } - deriving (Show, Generic, FromJSON) - -instance ToJSON User where - toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} - toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} + deriving (Show) data NewUser = NewUser { profile :: Maybe Profile, sameServers :: Bool, pastTimestamp :: Bool } - deriving (Show, Generic, FromJSON) + deriving (Show) newtype B64UrlByteString = B64UrlByteString ByteString deriving (Eq, Show) @@ -144,19 +142,13 @@ instance ToJSON B64UrlByteString where toEncoding = strToJEncoding data UserPwdHash = UserPwdHash {hash :: B64UrlByteString, salt :: B64UrlByteString} - deriving (Eq, Show, Generic, FromJSON) - -instance ToJSON UserPwdHash where toEncoding = J.genericToEncoding J.defaultOptions + deriving (Eq, Show) data UserInfo = UserInfo { user :: User, unreadCount :: Int } - deriving (Show, Generic, FromJSON) - -instance ToJSON UserInfo where - toJSON = J.genericToJSON J.defaultOptions - toEncoding = J.genericToEncoding J.defaultOptions + deriving (Show) type ContactId = Int64 @@ -179,11 +171,7 @@ data Contact = Contact contactGroupMemberId :: Maybe GroupMemberId, contactGrpInvSent :: Bool } - deriving (Eq, Show, Generic) - -instance ToJSON Contact where - toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} - toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} + deriving (Eq, Show) contactConn :: Contact -> Connection contactConn Contact {activeConn} = activeConn @@ -221,6 +209,9 @@ instance FromField ContactStatus where fromField = fromTextField_ textDecode instance ToField ContactStatus where toField = toField . textEncode +instance FromJSON ContactStatus where + parseJSON = textParseJSON "ContactStatus" + instance ToJSON ContactStatus where toJSON = J.String . textEncode toEncoding = JE.text . textEncode @@ -240,9 +231,7 @@ data ContactRef = ContactRef agentConnId :: AgentConnId, localDisplayName :: ContactName } - deriving (Eq, Show, Generic) - -instance ToJSON ContactRef where toEncoding = J.genericToEncoding J.defaultOptions + deriving (Eq, Show) data ContactOrGroupMember = CGMContact Contact | CGMGroupMember GroupInfo GroupMember deriving (Show) @@ -262,15 +251,13 @@ data UserContact = UserContact connReqContact :: ConnReqContact, groupId :: Maybe GroupId } - deriving (Eq, Show, Generic) + deriving (Eq, Show, Generic, FromJSON) + +instance ToJSON UserContact where toEncoding = J.genericToEncoding J.defaultOptions userContactGroupId :: UserContact -> Maybe GroupId userContactGroupId UserContact {groupId} = groupId -instance ToJSON UserContact where - toJSON = J.genericToJSON J.defaultOptions - toEncoding = J.genericToEncoding J.defaultOptions - data UserContactRequest = UserContactRequest { contactRequestId :: Int64, agentInvitationId :: AgentInvId, @@ -284,7 +271,7 @@ data UserContactRequest = UserContactRequest updatedAt :: UTCTime, xContactId :: Maybe XContactId } - deriving (Eq, Show, Generic) + deriving (Eq, Show, Generic, FromJSON) instance ToJSON UserContactRequest where toEncoding = J.genericToEncoding J.defaultOptions @@ -341,7 +328,7 @@ optionalFullName displayName fullName | otherwise = " (" <> fullName <> ")" data Group = Group {groupInfo :: GroupInfo, members :: [GroupMember]} - deriving (Eq, Show, Generic) + deriving (Eq, Show, Generic, FromJSON) instance ToJSON Group where toEncoding = J.genericToEncoding J.defaultOptions @@ -359,7 +346,7 @@ data GroupInfo = GroupInfo updatedAt :: UTCTime, chatTs :: Maybe UTCTime } - deriving (Eq, Show, Generic) + deriving (Eq, Show, Generic, FromJSON) instance ToJSON GroupInfo where toEncoding = J.genericToEncoding J.defaultOptions @@ -369,7 +356,7 @@ groupName' GroupInfo {localDisplayName = g} = g data GroupSummary = GroupSummary { currentMembers :: Int } - deriving (Show, Generic) + deriving (Show, Generic, FromJSON) instance ToJSON GroupSummary where toEncoding = J.genericToEncoding J.defaultOptions @@ -639,7 +626,7 @@ data GroupMember = GroupMember memberContactProfileId :: ProfileId, activeConn :: Maybe Connection } - deriving (Eq, Show, Generic) + deriving (Eq, Show, Generic, FromJSON) instance ToJSON GroupMember where toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} @@ -710,6 +697,9 @@ instance ToJSON MemberId where 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" @@ -803,6 +793,9 @@ instance FromField GroupMemberCategory where fromField = fromTextField_ textDeco instance ToField GroupMemberCategory where toField = toField . textEncode +instance FromJSON GroupMemberCategory where + parseJSON = textParseJSON "GroupMemberCategory" + instance ToJSON GroupMemberCategory where toJSON = J.String . textEncode toEncoding = JE.text . textEncode @@ -840,6 +833,9 @@ instance FromField GroupMemberStatus where fromField = fromTextField_ textDecode instance ToField GroupMemberStatus where toField = toField . textEncode +instance FromJSON GroupMemberStatus where + parseJSON = textParseJSON "GroupMemberStatus" + instance ToJSON GroupMemberStatus where toJSON = J.String . textEncode toEncoding = JE.text . textEncode @@ -931,7 +927,7 @@ data SndFileTransfer = SndFileTransfer fileDescrId :: Maybe Int64, fileInline :: Maybe InlineFileMode } - deriving (Eq, Show, Generic) + deriving (Eq, Show, Generic, FromJSON) instance ToJSON SndFileTransfer where toEncoding = J.genericToEncoding J.defaultOptions @@ -997,7 +993,7 @@ instance FromField InlineFileMode where fromField = fromTextField_ textDecode instance ToField InlineFileMode where toField = toField . textEncode instance FromJSON InlineFileMode where - parseJSON = J.withText "InlineFileMode" $ maybe (fail "bad InlineFileMode") pure . textDecode + parseJSON = textParseJSON "InlineFileMode" instance ToJSON InlineFileMode where toJSON = J.String . textEncode @@ -1017,7 +1013,7 @@ data RcvFileTransfer = RcvFileTransfer -- SMP files are encrypted after all chunks are received cryptoArgs :: Maybe CryptoFileArgs } - deriving (Eq, Show, Generic) + deriving (Eq, Show, Generic, FromJSON) instance ToJSON RcvFileTransfer where toEncoding = J.genericToEncoding J.defaultOptions @@ -1026,7 +1022,7 @@ data XFTPRcvFile = XFTPRcvFile agentRcvFileId :: Maybe AgentRcvFileId, agentRcvFileDeleted :: Bool } - deriving (Eq, Show, Generic) + deriving (Eq, Show, Generic, FromJSON) instance ToJSON XFTPRcvFile where toEncoding = J.genericToEncoding J.defaultOptions @@ -1036,7 +1032,7 @@ data RcvFileDescr = RcvFileDescr fileDescrPartNo :: Int, fileDescrComplete :: Bool } - deriving (Eq, Show, Generic) + deriving (Eq, Show, Generic, FromJSON) instance ToJSON RcvFileDescr where toEncoding = J.genericToEncoding J.defaultOptions @@ -1048,6 +1044,9 @@ data RcvFileStatus | RFSCancelled (Maybe RcvFileInfo) deriving (Eq, Show, Generic) +instance FromJSON RcvFileStatus where + parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "RFS" + instance ToJSON RcvFileStatus where toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "RFS" toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "RFS" @@ -1065,7 +1064,7 @@ data RcvFileInfo = RcvFileInfo connId :: Maybe Int64, agentConnId :: Maybe AgentConnId } - deriving (Eq, Show, Generic) + deriving (Eq, Show, Generic, FromJSON) instance ToJSON RcvFileInfo where toEncoding = J.genericToEncoding J.defaultOptions @@ -1094,6 +1093,9 @@ instance StrEncoding AgentConnId where strDecode s = AgentConnId <$> strDecode s strP = AgentConnId <$> strP +instance FromJSON AgentConnId where + parseJSON = strParseJSON "AgentConnId" + instance ToJSON AgentConnId where toJSON = strToJSON toEncoding = strToJEncoding @@ -1110,6 +1112,9 @@ instance StrEncoding AgentSndFileId where strDecode s = AgentSndFileId <$> strDecode s strP = AgentSndFileId <$> strP +instance FromJSON AgentSndFileId where + parseJSON = strParseJSON "AgentSndFileId" + instance ToJSON AgentSndFileId where toJSON = strToJSON toEncoding = strToJEncoding @@ -1126,6 +1131,9 @@ instance StrEncoding AgentRcvFileId where strDecode s = AgentRcvFileId <$> strDecode s strP = AgentRcvFileId <$> strP +instance FromJSON AgentRcvFileId where + parseJSON = strParseJSON "AgentRcvFileId" + instance ToJSON AgentRcvFileId where toJSON = strToJSON toEncoding = strToJEncoding @@ -1142,6 +1150,9 @@ instance StrEncoding AgentInvId where strDecode s = AgentInvId <$> strDecode s strP = AgentInvId <$> strP +instance FromJSON AgentInvId where + parseJSON = strParseJSON "AgentInvId" + instance ToJSON AgentInvId where toJSON = strToJSON toEncoding = strToJEncoding @@ -1158,6 +1169,9 @@ data FileTransfer | FTRcv {rcvFileTransfer :: RcvFileTransfer} deriving (Show, Generic) +instance FromJSON FileTransfer where + parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "FT" + instance ToJSON FileTransfer where toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "FT" toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "FT" @@ -1172,7 +1186,7 @@ data FileTransferMeta = FileTransferMeta chunkSize :: Integer, cancelled :: Bool } - deriving (Eq, Show, Generic) + deriving (Eq, Show, Generic, FromJSON) instance ToJSON FileTransferMeta where toEncoding = J.genericToEncoding J.defaultOptions @@ -1182,7 +1196,7 @@ data XFTPSndFile = XFTPSndFile agentSndFileDeleted :: Bool, cryptoArgs :: Maybe CryptoFileArgs } - deriving (Eq, Show, Generic) + deriving (Eq, Show, Generic, FromJSON) instance ToJSON XFTPSndFile where toEncoding = J.genericToEncoding J.defaultOptions @@ -1197,6 +1211,9 @@ instance FromField FileStatus where fromField = fromTextField_ textDecode instance ToField FileStatus where toField = toField . textEncode +instance FromJSON FileStatus where + parseJSON = textParseJSON "FileStatus" + instance ToJSON FileStatus where toJSON = J.String . textEncode toEncoding = JE.text . textEncode @@ -1250,11 +1267,9 @@ connDisabled :: Connection -> Bool connDisabled Connection {authErrCounter} = authErrCounter >= authErrDisableCount data SecurityCode = SecurityCode {securityCode :: Text, verifiedAt :: UTCTime} - deriving (Eq, Show, Generic) + deriving (Eq, Show, Generic, FromJSON) -instance ToJSON SecurityCode where - toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} - toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} +instance ToJSON SecurityCode where toEncoding = J.genericToEncoding J.defaultOptions verificationCode :: ByteString -> Text verificationCode = T.pack . unwords . chunks 5 . show . os2ip @@ -1273,6 +1288,9 @@ aConnId Connection {agentConnId = AgentConnId cId} = cId connIncognito :: Connection -> Bool connIncognito Connection {customUserProfileId} = isJust customUserProfileId +instance FromJSON Connection where + parseJSON = J.genericParseJSON J.defaultOptions {J.omitNothingFields = True} + instance ToJSON Connection where toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} @@ -1290,7 +1308,7 @@ data PendingContactConnection = PendingContactConnection createdAt :: UTCTime, updatedAt :: UTCTime } - deriving (Eq, Show, Generic) + deriving (Eq, Show, Generic, FromJSON) aConnId' :: PendingContactConnection -> ConnId aConnId' PendingContactConnection {pccAgentConnId = AgentConnId cId} = cId @@ -1318,6 +1336,9 @@ instance FromField ConnStatus where fromField = fromTextField_ textDecode instance ToField ConnStatus where toField = toField . textEncode +instance FromJSON ConnStatus where + parseJSON = textParseJSON "ConnStatus" + instance ToJSON ConnStatus where toJSON = J.String . textEncode toEncoding = JE.text . textEncode @@ -1348,6 +1369,9 @@ instance FromField ConnType where fromField = fromTextField_ textDecode instance ToField ConnType where toField = toField . textEncode +instance FromJSON ConnType where + parseJSON = textParseJSON "ConnType" + instance ToJSON ConnType where toJSON = J.String . textEncode toEncoding = JE.text . textEncode @@ -1550,6 +1574,24 @@ instance ToJSON ChatVersionRange where newtype JVersionRange = JVersionRange {fromJVersionRange :: VersionRange} deriving (Eq, Show) +instance FromJSON JVersionRange where + parseJSON = J.withObject "JVersionRange" $ \o -> do + minv <- o .: "minVersion" + maxv <- o .: "maxVersion" + maybe (fail "bad version range") (pure . JVersionRange) $ safeVersionRange minv maxv + instance ToJSON JVersionRange where toJSON (JVersionRange (VersionRange minV maxV)) = J.object ["minVersion" .= minV, "maxVersion" .= maxV] toEncoding (JVersionRange (VersionRange minV maxV)) = J.pairs $ "minVersion" .= minV <> "maxVersion" .= maxV + +$(JQ.deriveJSON defOpts ''UserPwdHash) + +$(JQ.deriveJSON defOpts ''User) + +$(JQ.deriveJSON defOpts ''NewUser) + +$(JQ.deriveJSON defOpts ''UserInfo) + +$(JQ.deriveJSON defOpts ''Contact) + +$(JQ.deriveJSON defOpts ''ContactRef) diff --git a/src/Simplex/Chat/Types/Preferences.hs b/src/Simplex/Chat/Types/Preferences.hs index c53e4476f4..c7555e18a8 100644 --- a/src/Simplex/Chat/Types/Preferences.hs +++ b/src/Simplex/Chat/Types/Preferences.hs @@ -338,7 +338,7 @@ data ContactUserPreferences = ContactUserPreferences voice :: ContactUserPreference VoicePreference, calls :: ContactUserPreference CallsPreference } - deriving (Eq, Show, Generic) + deriving (Eq, Show, Generic, FromJSON) data ContactUserPreference p = ContactUserPreference { enabled :: PrefEnabled, @@ -352,8 +352,13 @@ data ContactUserPref p = CUPContact {preference :: p} | CUPUser {preference :: p instance ToJSON ContactUserPreferences where toEncoding = J.genericToEncoding J.defaultOptions +instance FromJSON p => FromJSON (ContactUserPreference p) where parseJSON = J.genericParseJSON J.defaultOptions + instance ToJSON p => ToJSON (ContactUserPreference p) where toEncoding = J.genericToEncoding J.defaultOptions +instance FromJSON p => FromJSON (ContactUserPref p) where + parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "CUP" + instance ToJSON p => ToJSON (ContactUserPref p) where toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "CUP" toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "CUP" diff --git a/src/Simplex/Chat/Types/Util.hs b/src/Simplex/Chat/Types/Util.hs index fffdd24b9e..8681e99086 100644 --- a/src/Simplex/Chat/Types/Util.hs +++ b/src/Simplex/Chat/Types/Util.hs @@ -28,3 +28,6 @@ fromBlobField_ p = \case Right k -> Ok k Left e -> returnError ConversionFailed f ("could not parse field: " ++ e) f -> returnError ConversionFailed f "expecting SQLBlob column type" + +defOpts :: J.Options +defOpts = J.defaultOptions {J.omitNothingFields = True} diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 842a84cc65..f32b1835ed 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -73,10 +73,10 @@ responseToView :: Maybe User -> ChatConfig -> Bool -> CurrentTime -> TimeZone -> responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView} liveItems ts tz = \case CRActiveUser User {profile} -> viewUserProfile $ fromLocalProfile profile CRUsersList users -> viewUsersList users - CRChatStarted -> ["chat started"] - CRChatRunning -> ["chat is running"] - CRChatStopped -> ["chat stopped"] - CRChatSuspended -> ["chat suspended"] + CRChatStarted _ -> ["chat started"] + CRChatRunning _ -> ["chat is running"] + CRChatStopped _ -> ["chat stopped"] + CRChatSuspended _ -> ["chat suspended"] CRApiChats u chats -> ttyUser u $ if testView then testViewChats chats else [plain . bshow $ J.encode chats] CRChats chats -> viewChats ts tz chats CRApiChat u chat -> ttyUser u $ if testView then testViewChat chat else [plain . bshow $ J.encode chat] @@ -267,7 +267,7 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView CRRemoteHostDeleted rhId -> ["remote host " <> sShow rhId <> " deleted"] CRRemoteCtrlList cs -> viewRemoteCtrls cs CRRemoteCtrlRegistered rcId -> ["remote controller " <> sShow rcId <> " registered"] - CRRemoteCtrlStarted -> ["remote controller started"] + CRRemoteCtrlStarted _ -> ["remote controller started"] CRRemoteCtrlAnnounce fingerprint -> ["remote controller announced", "connection code:", plain $ strEncode fingerprint] CRRemoteCtrlFound rc -> ["remote controller found:", viewRemoteCtrl rc] CRRemoteCtrlAccepted rcId -> ["remote controller " <> sShow rcId <> " accepted"] diff --git a/stack.yaml b/stack.yaml index bce5dd3a68..9a343bcad2 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: ec1b72cb8013a65a5d9783104a47ae44f5730089 + commit: 96a38505d63ec9a12096991e7725b250e397af72 - github: kazu-yamamoto/http2 commit: b5a1b7200cf5bc7044af34ba325284271f6dff25 # - ../direct-sqlcipher