{-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} {-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-} module Simplex.Messaging.Agent.Store where import Control.Exception (Exception) import qualified Data.Attoparsec.ByteString.Char8 as A import Data.ByteString.Char8 (ByteString) import Data.Int (Int64) import Data.Kind (Type) import Data.List (find) import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as L import Data.Maybe (isJust) import Data.Time (UTCTime) import Data.Type.Equality import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.RetryInterval (RI2State) import Simplex.Messaging.Agent.Store.Common import Simplex.Messaging.Agent.Store.Interface (createDBStore) import Simplex.Messaging.Agent.Store.Migrations.App (appMigrations) import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation (..), MigrationError (..)) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet (MsgEncryptKeyX448, PQEncryption, PQSupport, RatchetX448) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Protocol ( MsgBody, MsgFlags, MsgId, NotifierId, NtfPrivateAuthKey, NtfPublicAuthKey, QueueMode, RcvDhSecret, RcvNtfDhSecret, RcvPrivateAuthKey, SndPrivateAuthKey, SndPublicAuthKey, VersionSMPC, ) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Agent.Store.Entity createStore :: DBOpts -> MigrationConfirmation -> IO (Either MigrationError DBStore) createStore dbOpts = createDBStore dbOpts appMigrations -- * Queue types type RcvQueue = StoredRcvQueue 'DBStored type NewRcvQueue = StoredRcvQueue 'DBNew -- | A receive queue. SMP queue through which the agent receives messages from a sender. data StoredRcvQueue (q :: DBStored) = RcvQueue { userId :: UserId, connId :: ConnId, server :: SMPServer, -- | recipient queue ID rcvId :: SMP.RecipientId, -- | key used by the recipient to authorize transmissions rcvPrivateKey :: RcvPrivateAuthKey, -- | shared DH secret used to encrypt/decrypt message bodies from server to recipient rcvDhSecret :: RcvDhSecret, -- | private DH key related to public sent to sender out-of-band (to agree simple per-queue e2e) e2ePrivKey :: C.PrivateKeyX25519, -- | public sender's DH key and agreed shared DH secret for simple per-queue e2e e2eDhSecret :: Maybe C.DhSecretX25519, -- | sender queue ID sndId :: SMP.SenderId, -- | sender can secure the queue queueMode :: Maybe QueueMode, -- | short link ID and credentials shortLink :: Maybe ShortLinkCreds, -- | associated client service clientService :: Maybe (StoredClientService q), -- | queue status status :: QueueStatus, -- | database queue ID (within connection) dbQueueId :: DBEntityId' q, -- | True for a primary or a next primary queue of the connection (next if dbReplaceQueueId is set) primary :: Bool, -- | database queue ID to replace, Nothing if this queue is not replacing another, `Just Nothing` is used for replacing old queues dbReplaceQueueId :: Maybe Int64, rcvSwchStatus :: Maybe RcvSwitchStatus, -- | SMP client version smpClientVersion :: VersionSMPC, -- | credentials used in context of notifications clientNtfCreds :: Maybe ClientNtfCreds, deleteErrors :: Int } deriving (Show) data ShortLinkCreds = ShortLinkCreds { shortLinkId :: SMP.LinkId, shortLinkKey :: LinkKey, linkPrivSigKey :: C.PrivateKeyEd25519, linkEncFixedData :: SMP.EncFixedDataBytes } deriving (Show) clientServiceId :: RcvQueue -> Maybe ClientServiceId clientServiceId = fmap dbServiceId . clientService {-# INLINE clientServiceId #-} rcvQueueInfo :: RcvQueue -> RcvQueueInfo rcvQueueInfo rq@RcvQueue {server, rcvSwchStatus} = RcvQueueInfo {rcvServer = server, rcvSwitchStatus = rcvSwchStatus, canAbortSwitch = canAbortRcvSwitch rq} rcvSMPQueueAddress :: RcvQueue -> SMPQueueAddress rcvSMPQueueAddress RcvQueue {server, sndId, e2ePrivKey, queueMode} = SMPQueueAddress server sndId (C.publicKey e2ePrivKey) queueMode canAbortRcvSwitch :: RcvQueue -> Bool canAbortRcvSwitch = maybe False canAbort . rcvSwchStatus where canAbort = \case RSSwitchStarted -> True RSSendingQADD -> True -- if switch is in RSSendingQUSE, a race condition with sender deleting the original queue is possible RSSendingQUSE -> False -- if switch is in RSReceivedMessage status, aborting switch (deleting new queue) -- will break the connection because the sender would have original queue deleted RSReceivedMessage -> False data ClientNtfCreds = ClientNtfCreds { -- | key pair to be used by the notification server to authorize transmissions ntfPublicKey :: NtfPublicAuthKey, ntfPrivateKey :: NtfPrivateAuthKey, -- | queue ID to be used by the notification server for NSUB command notifierId :: NotifierId, -- | shared DH secret used to encrypt/decrypt notification metadata (NMsgMeta) from server to recipient rcvNtfDhSecret :: RcvNtfDhSecret } deriving (Show) -- This record is stored in inv_short_links table. -- It is needed only for 1-time invitation links because of "secure-on-read" property of link data, -- that prevents undetected access to link data from link observers. data InvShortLink = InvShortLink { server :: SMPServer, linkId :: SMP.LinkId, linkKey :: LinkKey, sndPrivateKey :: SndPrivateAuthKey, -- stored to allow retries sndPublicKey :: SndPublicAuthKey, sndId :: Maybe SMP.SenderId } deriving (Show) type SndQueue = StoredSndQueue 'DBStored type NewSndQueue = StoredSndQueue 'DBNew -- | A send queue. SMP queue through which the agent sends messages to a recipient. data StoredSndQueue (q :: DBStored) = SndQueue { userId :: UserId, connId :: ConnId, server :: SMPServer, -- | sender queue ID sndId :: SMP.SenderId, -- | sender can secure the queue queueMode :: Maybe QueueMode, -- | key pair used by the sender to authorize transmissions -- TODO combine keys to key pair so that types match sndPublicKey :: SndPublicAuthKey, sndPrivateKey :: SndPrivateAuthKey, -- | DH public key used to negotiate per-queue e2e encryption e2ePubKey :: Maybe C.PublicKeyX25519, -- | shared DH secret agreed for simple per-queue e2e encryption e2eDhSecret :: C.DhSecretX25519, -- | queue status status :: QueueStatus, -- | database queue ID (within connection) dbQueueId :: DBEntityId' q, -- | True for a primary or a next primary queue of the connection (next if dbReplaceQueueId is set) primary :: Bool, -- | ID of the queue this one is replacing dbReplaceQueueId :: Maybe Int64, sndSwchStatus :: Maybe SndSwitchStatus, -- | SMP client version smpClientVersion :: VersionSMPC } deriving (Show) sndQueueInfo :: SndQueue -> SndQueueInfo sndQueueInfo SndQueue {server, sndSwchStatus} = SndQueueInfo {sndServer = server, sndSwitchStatus = sndSwchStatus} instance SMPQueue RcvQueue where qServer RcvQueue {server} = server {-# INLINE qServer #-} queueId RcvQueue {rcvId} = rcvId {-# INLINE queueId #-} instance SMPQueue NewRcvQueue where qServer RcvQueue {server} = server {-# INLINE qServer #-} queueId RcvQueue {rcvId} = rcvId {-# INLINE queueId #-} instance SMPQueue SndQueue where qServer SndQueue {server} = server {-# INLINE qServer #-} queueId SndQueue {sndId} = sndId {-# INLINE queueId #-} findQ :: SMPQueue q => (SMPServer, SMP.QueueId) -> NonEmpty q -> Maybe q findQ = find . sameQueue {-# INLINE findQ #-} removeQ :: SMPQueue q => (SMPServer, SMP.QueueId) -> NonEmpty q -> Maybe (q, [q]) removeQ = removeQP . sameQueue {-# INLINE removeQ #-} removeQP :: (q -> Bool) -> NonEmpty q -> Maybe (q, [q]) removeQP p qs = case L.break p qs of (_, []) -> Nothing (qs1, q : qs2) -> Just (q, qs1 <> qs2) sndAddress :: RcvQueue -> (SMPServer, SMP.SenderId) sndAddress RcvQueue {server, sndId} = (server, sndId) {-# INLINE sndAddress #-} findRQ :: (SMPServer, SMP.SenderId) -> NonEmpty RcvQueue -> Maybe RcvQueue findRQ sAddr = find $ sameQAddress sAddr . sndAddress {-# INLINE findRQ #-} switchingRQ :: NonEmpty RcvQueue -> Maybe RcvQueue switchingRQ = find $ isJust . rcvSwchStatus {-# INLINE switchingRQ #-} updatedQs :: SMPQueueRec q => q -> NonEmpty q -> NonEmpty q updatedQs q = L.map $ \q' -> if dbQId q == dbQId q' then q else q' {-# INLINE updatedQs #-} class SMPQueue q => SMPQueueRec q where qUserId :: q -> UserId qConnId :: q -> ConnId dbQId :: q -> Int64 dbReplaceQId :: q -> Maybe Int64 instance SMPQueueRec RcvQueue where qUserId RcvQueue {userId} = userId {-# INLINE qUserId #-} qConnId RcvQueue {connId} = connId {-# INLINE qConnId #-} dbQId RcvQueue {dbQueueId = DBEntityId qId} = qId {-# INLINE dbQId #-} dbReplaceQId RcvQueue {dbReplaceQueueId} = dbReplaceQueueId {-# INLINE dbReplaceQId #-} instance SMPQueueRec SndQueue where qUserId SndQueue {userId} = userId {-# INLINE qUserId #-} qConnId SndQueue {connId} = connId {-# INLINE qConnId #-} dbQId SndQueue {dbQueueId = DBEntityId qId} = qId {-# INLINE dbQId #-} dbReplaceQId SndQueue {dbReplaceQueueId} = dbReplaceQueueId {-# INLINE dbReplaceQId #-} -- * Connection types -- | Type of a connection. data ConnType = CNew | CRcv | CSnd | CDuplex | CContact deriving (Eq, Show) -- | Connection of a specific type. -- -- - RcvConnection is a connection that only has a receive queue set up, -- typically created by a recipient initiating a duplex connection. -- -- - SndConnection is a connection that only has a send queue set up, typically -- created by a sender joining a duplex connection through a recipient's invitation. -- -- - DuplexConnection is a connection that has both receive and send queues set up, -- typically created by upgrading a receive or a send connection with a missing queue. data Connection (d :: ConnType) where NewConnection :: ConnData -> Connection CNew RcvConnection :: ConnData -> RcvQueue -> Connection CRcv SndConnection :: ConnData -> SndQueue -> Connection CSnd DuplexConnection :: ConnData -> NonEmpty RcvQueue -> NonEmpty SndQueue -> Connection CDuplex ContactConnection :: ConnData -> RcvQueue -> Connection CContact deriving instance Show (Connection d) toConnData :: Connection d -> ConnData toConnData = \case NewConnection cData -> cData RcvConnection cData _ -> cData SndConnection cData _ -> cData DuplexConnection cData _ _ -> cData ContactConnection cData _ -> cData updateConnection :: ConnData -> Connection d -> Connection d updateConnection cData = \case NewConnection _ -> NewConnection cData RcvConnection _ rq -> RcvConnection cData rq SndConnection _ sq -> SndConnection cData sq DuplexConnection _ rqs sqs -> DuplexConnection cData rqs sqs ContactConnection _ rq -> ContactConnection cData rq data SConnType :: ConnType -> Type where SCNew :: SConnType CNew SCRcv :: SConnType CRcv SCSnd :: SConnType CSnd SCDuplex :: SConnType CDuplex SCContact :: SConnType CContact connType :: SConnType c -> ConnType connType SCNew = CNew connType SCRcv = CRcv connType SCSnd = CSnd connType SCDuplex = CDuplex connType SCContact = CContact deriving instance Show (SConnType d) instance TestEquality SConnType where testEquality SCRcv SCRcv = Just Refl testEquality SCSnd SCSnd = Just Refl testEquality SCDuplex SCDuplex = Just Refl testEquality SCContact SCContact = Just Refl testEquality _ _ = Nothing -- | Connection of an unknown type. -- Used to refer to an arbitrary connection when retrieving from store. data SomeConn = forall d. SomeConn (SConnType d) (Connection d) deriving instance Show SomeConn data ConnData = ConnData { connId :: ConnId, userId :: UserId, connAgentVersion :: VersionSMPA, enableNtfs :: Bool, lastExternalSndId :: PrevExternalSndId, deleted :: Bool, ratchetSyncState :: RatchetSyncState, pqSupport :: PQSupport } deriving (Eq, Show) -- this function should be mirrored in the clients ratchetSyncAllowed :: ConnData -> Bool ratchetSyncAllowed ConnData {ratchetSyncState, connAgentVersion} = connAgentVersion >= ratchetSyncSMPAgentVersion && (ratchetSyncState `elem` ([RSAllowed, RSRequired] :: [RatchetSyncState])) -- this function should be mirrored in the clients ratchetSyncSendProhibited :: ConnData -> Bool ratchetSyncSendProhibited ConnData {ratchetSyncState} = ratchetSyncState `elem` ([RSRequired, RSStarted, RSAgreed] :: [RatchetSyncState]) data PendingCommand = PendingCommand { cmdId :: AsyncCmdId, corrId :: ACorrId, userId :: UserId, connId :: ConnId, command :: AgentCommand } data AgentCmdType = ACClient | ACInternal instance StrEncoding AgentCmdType where strEncode = \case ACClient -> "CLIENT" ACInternal -> "INTERNAL" strP = A.takeTill (== ' ') >>= \case "CLIENT" -> pure ACClient "INTERNAL" -> pure ACInternal _ -> fail "bad AgentCmdType" data AgentCommand = AClientCommand ACommand | AInternalCommand InternalCommand instance StrEncoding AgentCommand where strEncode = \case AClientCommand cmd -> strEncode (ACClient, Str $ serializeCommand cmd) AInternalCommand cmd -> strEncode (ACInternal, cmd) strP = strP_ >>= \case ACClient -> AClientCommand <$> dbCommandP ACInternal -> AInternalCommand <$> strP data AgentCommandTag = AClientCommandTag ACommandTag | AInternalCommandTag InternalCommandTag deriving (Show) instance StrEncoding AgentCommandTag where strEncode = \case AClientCommandTag t -> strEncode (ACClient, t) AInternalCommandTag t -> strEncode (ACInternal, t) strP = strP_ >>= \case ACClient -> AClientCommandTag <$> strP ACInternal -> AInternalCommandTag <$> strP data InternalCommand = ICAck SMP.RecipientId MsgId | ICAckDel SMP.RecipientId MsgId InternalId | ICAllowSecure SMP.RecipientId (Maybe SMP.SndPublicAuthKey) | ICDuplexSecure SMP.RecipientId SMP.SndPublicAuthKey | ICDeleteConn | ICDeleteRcvQueue SMP.RecipientId | ICQSecure SMP.RecipientId SMP.SndPublicAuthKey | ICQDelete SMP.RecipientId data InternalCommandTag = ICAck_ | ICAckDel_ | ICAllowSecure_ | ICDuplexSecure_ | ICDeleteConn_ | ICDeleteRcvQueue_ | ICQSecure_ | ICQDelete_ deriving (Show) instance StrEncoding InternalCommand where strEncode = \case ICAck rId srvMsgId -> strEncode (ICAck_, rId, srvMsgId) ICAckDel rId srvMsgId mId -> strEncode (ICAckDel_, rId, srvMsgId, mId) ICAllowSecure rId sndKey -> strEncode (ICAllowSecure_, rId, sndKey) ICDuplexSecure rId sndKey -> strEncode (ICDuplexSecure_, rId, sndKey) ICDeleteConn -> strEncode ICDeleteConn_ ICDeleteRcvQueue rId -> strEncode (ICDeleteRcvQueue_, rId) ICQSecure rId senderKey -> strEncode (ICQSecure_, rId, senderKey) ICQDelete rId -> strEncode (ICQDelete_, rId) strP = strP >>= \case ICAck_ -> ICAck <$> _strP <*> _strP ICAckDel_ -> ICAckDel <$> _strP <*> _strP <*> _strP ICAllowSecure_ -> ICAllowSecure <$> _strP <*> _strP ICDuplexSecure_ -> ICDuplexSecure <$> _strP <*> _strP ICDeleteConn_ -> pure ICDeleteConn ICDeleteRcvQueue_ -> ICDeleteRcvQueue <$> _strP ICQSecure_ -> ICQSecure <$> _strP <*> _strP ICQDelete_ -> ICQDelete <$> _strP instance StrEncoding InternalCommandTag where strEncode = \case ICAck_ -> "ACK" ICAckDel_ -> "ACK_DEL" ICAllowSecure_ -> "ALLOW_SECURE" ICDuplexSecure_ -> "DUPLEX_SECURE" ICDeleteConn_ -> "DELETE_CONN" ICDeleteRcvQueue_ -> "DELETE_RCV_QUEUE" ICQSecure_ -> "QSECURE" ICQDelete_ -> "QDELETE" strP = A.takeTill (== ' ') >>= \case "ACK" -> pure ICAck_ "ACK_DEL" -> pure ICAckDel_ "ALLOW_SECURE" -> pure ICAllowSecure_ "DUPLEX_SECURE" -> pure ICDuplexSecure_ "DELETE_CONN" -> pure ICDeleteConn_ "DELETE_RCV_QUEUE" -> pure ICDeleteRcvQueue_ "QSECURE" -> pure ICQSecure_ "QDELETE" -> pure ICQDelete_ _ -> fail "bad InternalCommandTag" agentCommandTag :: AgentCommand -> AgentCommandTag agentCommandTag = \case AClientCommand cmd -> AClientCommandTag $ aCommandTag cmd AInternalCommand cmd -> AInternalCommandTag $ internalCmdTag cmd internalCmdTag :: InternalCommand -> InternalCommandTag internalCmdTag = \case ICAck {} -> ICAck_ ICAckDel {} -> ICAckDel_ ICAllowSecure {} -> ICAllowSecure_ ICDuplexSecure {} -> ICDuplexSecure_ ICDeleteConn -> ICDeleteConn_ ICDeleteRcvQueue {} -> ICDeleteRcvQueue_ ICQSecure {} -> ICQSecure_ ICQDelete _ -> ICQDelete_ -- * Confirmation types data NewConfirmation = NewConfirmation { connId :: ConnId, senderConf :: SMPConfirmation, ratchetState :: RatchetX448 } data AcceptedConfirmation = AcceptedConfirmation { confirmationId :: ConfirmationId, connId :: ConnId, senderConf :: SMPConfirmation, ratchetState :: RatchetX448, ownConnInfo :: ConnInfo } -- * Invitations data NewInvitation = NewInvitation { contactConnId :: ConnId, connReq :: ConnectionRequestUri 'CMInvitation, recipientConnInfo :: ConnInfo } data Invitation = Invitation { invitationId :: InvitationId, contactConnId :: ConnId, connReq :: ConnectionRequestUri 'CMInvitation, recipientConnInfo :: ConnInfo, ownConnInfo :: Maybe ConnInfo, accepted :: Bool } -- * Message integrity validation types -- | Corresponds to `last_external_snd_msg_id` in `connections` table type PrevExternalSndId = Int64 -- | Corresponds to `last_rcv_msg_hash` in `connections` table type PrevRcvMsgHash = MsgHash -- | Corresponds to `last_snd_msg_hash` in `connections` table type PrevSndMsgHash = MsgHash -- * Message data containers data RcvMsgData = RcvMsgData { msgMeta :: MsgMeta, msgType :: AgentMessageType, msgFlags :: MsgFlags, msgBody :: MsgBody, internalRcvId :: InternalRcvId, internalHash :: MsgHash, externalPrevSndHash :: MsgHash, encryptedMsgHash :: MsgHash } data RcvMsg = RcvMsg { internalId :: InternalId, msgMeta :: MsgMeta, msgType :: AgentMessageType, msgBody :: MsgBody, internalHash :: MsgHash, msgReceipt :: Maybe MsgReceipt, -- if this message is a delivery receipt userAck :: Bool } data SndMsgData = SndMsgData { internalId :: InternalId, internalSndId :: InternalSndId, internalTs :: InternalTs, msgType :: AgentMessageType, msgFlags :: MsgFlags, msgBody :: MsgBody, pqEncryption :: PQEncryption, internalHash :: MsgHash, prevMsgHash :: MsgHash, sndMsgPrepData_ :: Maybe SndMsgPrepData } data SndMsgPrepData = SndMsgPrepData { encryptKey :: MsgEncryptKeyX448, paddedLen :: Int, sndMsgBodyId :: Int64 } deriving (Show) data SndMsg = SndMsg { internalId :: InternalId, internalSndId :: InternalSndId, msgType :: AgentMessageType, internalHash :: MsgHash, msgReceipt :: Maybe MsgReceipt } data PendingMsgData = PendingMsgData { msgId :: InternalId, msgType :: AgentMessageType, msgFlags :: MsgFlags, msgBody :: MsgBody, pqEncryption :: PQEncryption, msgRetryState :: Maybe RI2State, internalTs :: InternalTs, internalSndId :: InternalSndId, prevMsgHash :: PrevSndMsgHash, pendingMsgPrepData_ :: Maybe PendingMsgPrepData } deriving (Show) data PendingMsgPrepData = PendingMsgPrepData { encryptKey :: MsgEncryptKeyX448, paddedLen :: Int, sndMsgBody :: AMessage } deriving (Show) -- internal Ids are newtypes to prevent mixing them up newtype InternalRcvId = InternalRcvId {unRcvId :: Int64} deriving (Eq, Show) type ExternalSndId = Int64 type ExternalSndTs = UTCTime type BrokerId = MsgId type BrokerTs = UTCTime newtype InternalSndId = InternalSndId {unSndId :: Int64} deriving (Eq, Show) -- | Base message data independent of direction. data MsgBase = MsgBase { connId :: ConnId, -- | Monotonically increasing id of a message per connection, internal to the agent. -- Internal Id preserves ordering between both received and sent messages, and is needed -- to track the order of the conversation (which can be different for the sender / receiver) -- and address messages in commands. External [sender] Id cannot be used for this purpose -- due to a possibility of implementation errors in different agents. internalId :: InternalId, internalTs :: InternalTs, msgBody :: MsgBody, -- | Hash of the message as computed by agent. internalHash :: MsgHash } deriving (Eq, Show) newtype InternalId = InternalId {unId :: Int64} deriving (Eq, Show) instance StrEncoding InternalId where strEncode = strEncode . unId strP = InternalId <$> strP type InternalTs = UTCTime type AsyncCmdId = Int64 -- * Store errors -- | Agent store error. data StoreError = -- | IO exceptions in store actions. SEInternal ByteString | -- | Database busy SEDatabaseBusy ByteString | -- | Failed to generate unique random ID SEUniqueID | -- | User ID not found SEUserNotFound | -- | Connection not found (or both queues absent). SEConnNotFound | -- | Server not found. SEServerNotFound | -- | Connection already used. SEConnDuplicate | -- | Confirmed snd queue already exists. SESndQueueExists | -- | Wrong connection type, e.g. "send" connection when "receive" or "duplex" is expected, or vice versa. -- 'upgradeRcvConnToDuplex' and 'upgradeSndConnToDuplex' do not allow duplex connections - they would also return this error. SEBadConnType String ConnType | -- | Confirmation not found. SEConfirmationNotFound | -- | Invitation not found SEInvitationNotFound String InvitationId | -- | Message not found SEMsgNotFound | -- | Command not found SECmdNotFound | -- | Currently not used. The intention was to pass current expected queue status in methods, -- as we always know what it should be at any stage of the protocol, -- and in case it does not match use this error. SEBadQueueStatus | -- | connection does not have associated double-ratchet state SERatchetNotFound | -- | connection does not have associated x3dh keys SEX3dhKeysNotFound | -- | Used to wrap agent errors inside store operations to avoid race conditions SEAgentError AgentErrorType | -- | XFTP Server not found. SEXFTPServerNotFound | -- | XFTP File not found. SEFileNotFound | -- | XFTP Deleted snd chunk replica not found. SEDeletedSndChunkReplicaNotFound | -- | Error when reading work item that suspends worker - do not use! SEWorkItemError ByteString | -- | Servers stats not found. SEServersStatsNotFound deriving (Eq, Show, Exception)