mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-03-30 18:35:59 +00:00
653 lines
20 KiB
Haskell
653 lines
20 KiB
Haskell
{-# LANGUAGE DataKinds #-}
|
|
{-# LANGUAGE GADTs #-}
|
|
{-# LANGUAGE LambdaCase #-}
|
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
{-# LANGUAGE StandaloneDeriving #-}
|
|
{-# LANGUAGE TypeApplications #-}
|
|
{-# LANGUAGE TypeFamilies #-}
|
|
|
|
module Simplex.Messaging.Notifications.Protocol
|
|
( NtfEntity (..),
|
|
SNtfEntity (..),
|
|
NtfEntityI (..),
|
|
NtfCommandTag (..),
|
|
NtfCmdTag (..),
|
|
NtfRegCode (..),
|
|
NewNtfEntity (..),
|
|
ANewNtfEntity (..),
|
|
NtfCommand (..),
|
|
NtfCmd (..),
|
|
NtfResponseTag (..),
|
|
NtfResponse (..),
|
|
SMPQueueNtf (..),
|
|
PushProvider (..),
|
|
DeviceToken (..),
|
|
PNMessageData (..),
|
|
NtfEntityId,
|
|
NtfSubscriptionId,
|
|
NtfTokenId,
|
|
NtfSubStatus (..),
|
|
NtfTknStatus (..),
|
|
NTInvalidReason (..),
|
|
encodePNMessages,
|
|
pnMessagesP,
|
|
subscribeNtfStatuses,
|
|
allowTokenVerification,
|
|
allowNtfSubCommands,
|
|
checkEntity,
|
|
) where
|
|
|
|
import Control.Applicative (optional, (<|>))
|
|
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
|
|
import Data.ByteString.Char8 (ByteString)
|
|
import qualified Data.ByteString.Char8 as B
|
|
import Data.Functor (($>))
|
|
import Data.Kind
|
|
import Data.List.NonEmpty (NonEmpty (..))
|
|
import qualified Data.List.NonEmpty as L
|
|
import Data.Maybe (isNothing)
|
|
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
|
|
import Data.Time.Clock.System
|
|
import Data.Type.Equality
|
|
import Data.Word (Word16)
|
|
import Simplex.Messaging.Agent.Protocol (updateSMPServerHosts)
|
|
import Simplex.Messaging.Agent.Store.DB (FromField (..), ToField (..), fromTextField_)
|
|
import qualified Simplex.Messaging.Crypto as C
|
|
import Simplex.Messaging.Encoding
|
|
import Simplex.Messaging.Encoding.String
|
|
import Simplex.Messaging.Notifications.Transport (NTFVersion, invalidReasonNTFVersion, ntfClientHandshake)
|
|
import Simplex.Messaging.Protocol hiding (Command (..), CommandTag (..))
|
|
import Simplex.Messaging.Util (eitherToMaybe, (<$?>))
|
|
|
|
data NtfEntity = Token | Subscription
|
|
deriving (Show)
|
|
|
|
data SNtfEntity :: NtfEntity -> Type where
|
|
SToken :: SNtfEntity 'Token
|
|
SSubscription :: SNtfEntity 'Subscription
|
|
|
|
instance TestEquality SNtfEntity where
|
|
testEquality SToken SToken = Just Refl
|
|
testEquality SSubscription SSubscription = Just Refl
|
|
testEquality _ _ = Nothing
|
|
|
|
deriving instance Show (SNtfEntity e)
|
|
|
|
class NtfEntityI (e :: NtfEntity) where sNtfEntity :: SNtfEntity e
|
|
|
|
instance NtfEntityI 'Token where sNtfEntity = SToken
|
|
|
|
instance NtfEntityI 'Subscription where sNtfEntity = SSubscription
|
|
|
|
data NtfCommandTag (e :: NtfEntity) where
|
|
TNEW_ :: NtfCommandTag 'Token
|
|
TVFY_ :: NtfCommandTag 'Token
|
|
TCHK_ :: NtfCommandTag 'Token
|
|
TRPL_ :: NtfCommandTag 'Token
|
|
TDEL_ :: NtfCommandTag 'Token
|
|
TCRN_ :: NtfCommandTag 'Token
|
|
SNEW_ :: NtfCommandTag 'Subscription
|
|
SCHK_ :: NtfCommandTag 'Subscription
|
|
SDEL_ :: NtfCommandTag 'Subscription
|
|
PING_ :: NtfCommandTag 'Subscription
|
|
|
|
deriving instance Show (NtfCommandTag e)
|
|
|
|
data NtfCmdTag = forall e. NtfEntityI e => NCT (SNtfEntity e) (NtfCommandTag e)
|
|
|
|
instance NtfEntityI e => Encoding (NtfCommandTag e) where
|
|
smpEncode = \case
|
|
TNEW_ -> "TNEW"
|
|
TVFY_ -> "TVFY"
|
|
TCHK_ -> "TCHK"
|
|
TRPL_ -> "TRPL"
|
|
TDEL_ -> "TDEL"
|
|
TCRN_ -> "TCRN"
|
|
SNEW_ -> "SNEW"
|
|
SCHK_ -> "SCHK"
|
|
SDEL_ -> "SDEL"
|
|
PING_ -> "PING"
|
|
smpP = messageTagP
|
|
|
|
instance Encoding NtfCmdTag where
|
|
smpEncode (NCT _ t) = smpEncode t
|
|
smpP = messageTagP
|
|
|
|
instance ProtocolMsgTag NtfCmdTag where
|
|
decodeTag = \case
|
|
"TNEW" -> Just $ NCT SToken TNEW_
|
|
"TVFY" -> Just $ NCT SToken TVFY_
|
|
"TCHK" -> Just $ NCT SToken TCHK_
|
|
"TRPL" -> Just $ NCT SToken TRPL_
|
|
"TDEL" -> Just $ NCT SToken TDEL_
|
|
"TCRN" -> Just $ NCT SToken TCRN_
|
|
"SNEW" -> Just $ NCT SSubscription SNEW_
|
|
"SCHK" -> Just $ NCT SSubscription SCHK_
|
|
"SDEL" -> Just $ NCT SSubscription SDEL_
|
|
"PING" -> Just $ NCT SSubscription PING_
|
|
_ -> Nothing
|
|
|
|
instance NtfEntityI e => ProtocolMsgTag (NtfCommandTag e) where
|
|
decodeTag s = decodeTag s >>= (\(NCT _ t) -> checkEntity' t)
|
|
|
|
newtype NtfRegCode = NtfRegCode ByteString
|
|
deriving (Eq, Show)
|
|
|
|
instance Encoding NtfRegCode where
|
|
smpEncode (NtfRegCode code) = smpEncode code
|
|
smpP = NtfRegCode <$> smpP
|
|
|
|
instance StrEncoding NtfRegCode where
|
|
strEncode (NtfRegCode m) = strEncode m
|
|
strDecode s = NtfRegCode <$> strDecode s
|
|
strP = NtfRegCode <$> strP
|
|
|
|
instance FromJSON NtfRegCode where
|
|
parseJSON = strParseJSON "NtfRegCode"
|
|
|
|
instance ToJSON NtfRegCode where
|
|
toJSON = strToJSON
|
|
toEncoding = strToJEncoding
|
|
|
|
data NewNtfEntity (e :: NtfEntity) where
|
|
NewNtfTkn :: DeviceToken -> NtfPublicAuthKey -> C.PublicKeyX25519 -> NewNtfEntity 'Token
|
|
NewNtfSub :: NtfTokenId -> SMPQueueNtf -> NtfPrivateAuthKey -> NewNtfEntity 'Subscription
|
|
|
|
deriving instance Show (NewNtfEntity e)
|
|
|
|
data ANewNtfEntity = forall e. NtfEntityI e => ANE (SNtfEntity e) (NewNtfEntity e)
|
|
|
|
deriving instance Show ANewNtfEntity
|
|
|
|
instance NtfEntityI e => Encoding (NewNtfEntity e) where
|
|
smpEncode = \case
|
|
NewNtfTkn tkn verifyKey dhPubKey -> smpEncode ('T', tkn, verifyKey, dhPubKey)
|
|
NewNtfSub tknId smpQueue notifierKey -> smpEncode ('S', tknId, smpQueue, notifierKey)
|
|
smpP = (\(ANE _ c) -> checkEntity c) <$?> smpP
|
|
|
|
instance Encoding ANewNtfEntity where
|
|
smpEncode (ANE _ e) = smpEncode e
|
|
smpP =
|
|
A.anyChar >>= \case
|
|
'T' -> ANE SToken <$> (NewNtfTkn <$> smpP <*> smpP <*> smpP)
|
|
'S' -> ANE SSubscription <$> (NewNtfSub <$> smpP <*> smpP <*> smpP)
|
|
_ -> fail "bad ANewNtfEntity"
|
|
|
|
instance Protocol NTFVersion ErrorType NtfResponse where
|
|
type ProtoCommand NtfResponse = NtfCmd
|
|
type ProtoType NtfResponse = 'PNTF
|
|
protocolClientHandshake c _ks = ntfClientHandshake c
|
|
{-# INLINE protocolClientHandshake #-}
|
|
useServiceAuth _ = False
|
|
{-# INLINE useServiceAuth #-}
|
|
protocolPing = NtfCmd SSubscription PING
|
|
{-# INLINE protocolPing #-}
|
|
protocolError = \case
|
|
NRErr e -> Just e
|
|
_ -> Nothing
|
|
{-# INLINE protocolError #-}
|
|
|
|
data NtfCommand (e :: NtfEntity) where
|
|
-- | register new device token for notifications
|
|
TNEW :: NewNtfEntity 'Token -> NtfCommand 'Token
|
|
-- | verify token - uses e2e encrypted random string sent to the device via PN to confirm that the device has the token
|
|
TVFY :: NtfRegCode -> NtfCommand 'Token
|
|
-- | check token status
|
|
TCHK :: NtfCommand 'Token
|
|
-- | replace device token (while keeping all existing subscriptions)
|
|
TRPL :: DeviceToken -> NtfCommand 'Token
|
|
-- | delete token - all subscriptions will be removed and no more notifications will be sent
|
|
TDEL :: NtfCommand 'Token
|
|
-- | enable periodic background notification to fetch the new messages - interval is in minutes, minimum is 20, 0 to disable
|
|
TCRN :: Word16 -> NtfCommand 'Token
|
|
-- | create SMP subscription
|
|
SNEW :: NewNtfEntity 'Subscription -> NtfCommand 'Subscription
|
|
-- | check SMP subscription status (response is SUB)
|
|
SCHK :: NtfCommand 'Subscription
|
|
-- | delete SMP subscription
|
|
SDEL :: NtfCommand 'Subscription
|
|
-- | keep-alive command
|
|
PING :: NtfCommand 'Subscription
|
|
|
|
deriving instance Show (NtfCommand e)
|
|
|
|
data NtfCmd = forall e. NtfEntityI e => NtfCmd (SNtfEntity e) (NtfCommand e)
|
|
|
|
deriving instance Show NtfCmd
|
|
|
|
instance NtfEntityI e => ProtocolEncoding NTFVersion ErrorType (NtfCommand e) where
|
|
type Tag (NtfCommand e) = NtfCommandTag e
|
|
encodeProtocol _v = \case
|
|
TNEW newTkn -> e (TNEW_, ' ', newTkn)
|
|
TVFY code -> e (TVFY_, ' ', code)
|
|
TCHK -> e TCHK_
|
|
TRPL tkn -> e (TRPL_, ' ', tkn)
|
|
TDEL -> e TDEL_
|
|
TCRN int -> e (TCRN_, ' ', int)
|
|
SNEW newSub -> e (SNEW_, ' ', newSub)
|
|
SCHK -> e SCHK_
|
|
SDEL -> e SDEL_
|
|
PING -> e PING_
|
|
where
|
|
e :: Encoding a => a -> ByteString
|
|
e = smpEncode
|
|
|
|
protocolP _v tag = (\(NtfCmd _ c) -> checkEntity c) <$?> protocolP _v (NCT (sNtfEntity @e) tag)
|
|
|
|
fromProtocolError = fromProtocolError @NTFVersion @ErrorType @NtfResponse
|
|
{-# INLINE fromProtocolError #-}
|
|
|
|
checkCredentials auth (EntityId entityId) cmd = case cmd of
|
|
-- TNEW and SNEW must have signature but NOT token/subscription IDs
|
|
TNEW {} -> sigNoEntity
|
|
SNEW {} -> sigNoEntity
|
|
PING
|
|
| isNothing auth && B.null entityId -> Right cmd
|
|
| otherwise -> Left $ CMD HAS_AUTH
|
|
-- other client commands must have both signature and entity ID
|
|
_
|
|
| isNothing auth || B.null entityId -> Left $ CMD NO_AUTH
|
|
| otherwise -> Right cmd
|
|
where
|
|
sigNoEntity
|
|
| isNothing auth = Left $ CMD NO_AUTH
|
|
| not (B.null entityId) = Left $ CMD HAS_AUTH
|
|
| otherwise = Right cmd
|
|
|
|
instance ProtocolEncoding NTFVersion ErrorType NtfCmd where
|
|
type Tag NtfCmd = NtfCmdTag
|
|
encodeProtocol _v (NtfCmd _ c) = encodeProtocol _v c
|
|
|
|
protocolP _v = \case
|
|
NCT SToken tag ->
|
|
NtfCmd SToken <$> case tag of
|
|
TNEW_ -> TNEW <$> _smpP
|
|
TVFY_ -> TVFY <$> _smpP
|
|
TCHK_ -> pure TCHK
|
|
TRPL_ -> TRPL <$> _smpP
|
|
TDEL_ -> pure TDEL
|
|
TCRN_ -> TCRN <$> _smpP
|
|
NCT SSubscription tag ->
|
|
NtfCmd SSubscription <$> case tag of
|
|
SNEW_ -> SNEW <$> _smpP
|
|
SCHK_ -> pure SCHK
|
|
SDEL_ -> pure SDEL
|
|
PING_ -> pure PING
|
|
|
|
fromProtocolError = fromProtocolError @NTFVersion @ErrorType @NtfResponse
|
|
{-# INLINE fromProtocolError #-}
|
|
|
|
checkCredentials tAuth entId (NtfCmd e c) = NtfCmd e <$> checkCredentials tAuth entId c
|
|
|
|
data NtfResponseTag
|
|
= NRTknId_
|
|
| NRSubId_
|
|
| NROk_
|
|
| NRErr_
|
|
| NRTkn_
|
|
| NRSub_
|
|
| NRPong_
|
|
deriving (Show)
|
|
|
|
instance Encoding NtfResponseTag where
|
|
smpEncode = \case
|
|
NRTknId_ -> "IDTKN" -- it should be "TID", "SID"
|
|
NRSubId_ -> "IDSUB"
|
|
NROk_ -> "OK"
|
|
NRErr_ -> "ERR"
|
|
NRTkn_ -> "TKN"
|
|
NRSub_ -> "SUB"
|
|
NRPong_ -> "PONG"
|
|
smpP = messageTagP
|
|
|
|
instance ProtocolMsgTag NtfResponseTag where
|
|
decodeTag = \case
|
|
"IDTKN" -> Just NRTknId_
|
|
"IDSUB" -> Just NRSubId_
|
|
"OK" -> Just NROk_
|
|
"ERR" -> Just NRErr_
|
|
"TKN" -> Just NRTkn_
|
|
"SUB" -> Just NRSub_
|
|
"PONG" -> Just NRPong_
|
|
_ -> Nothing
|
|
|
|
data NtfResponse
|
|
= NRTknId NtfEntityId C.PublicKeyX25519
|
|
| NRSubId NtfEntityId
|
|
| NROk
|
|
| NRErr ErrorType
|
|
| NRTkn NtfTknStatus
|
|
| NRSub NtfSubStatus
|
|
| NRPong
|
|
deriving (Show)
|
|
|
|
instance ProtocolEncoding NTFVersion ErrorType NtfResponse where
|
|
type Tag NtfResponse = NtfResponseTag
|
|
encodeProtocol v = \case
|
|
NRTknId entId dhKey -> e (NRTknId_, ' ', entId, dhKey)
|
|
NRSubId entId -> e (NRSubId_, ' ', entId)
|
|
NROk -> e NROk_
|
|
NRErr err -> e (NRErr_, ' ', err)
|
|
NRTkn stat -> e (NRTkn_, ' ', stat')
|
|
where
|
|
stat'
|
|
| v >= invalidReasonNTFVersion = stat
|
|
| otherwise = case stat of
|
|
NTInvalid _ -> NTInvalid Nothing
|
|
_ -> stat
|
|
NRSub stat -> e (NRSub_, ' ', stat)
|
|
NRPong -> e NRPong_
|
|
where
|
|
e :: Encoding a => a -> ByteString
|
|
e = smpEncode
|
|
|
|
protocolP _v = \case
|
|
NRTknId_ -> NRTknId <$> _smpP <*> smpP
|
|
NRSubId_ -> NRSubId <$> _smpP
|
|
NROk_ -> pure NROk
|
|
NRErr_ -> NRErr <$> _smpP
|
|
NRTkn_ -> NRTkn <$> _smpP
|
|
NRSub_ -> NRSub <$> _smpP
|
|
NRPong_ -> pure NRPong
|
|
|
|
fromProtocolError = \case
|
|
PECmdSyntax -> CMD SYNTAX
|
|
PECmdUnknown -> CMD UNKNOWN
|
|
PESession -> SESSION
|
|
PEBlock -> BLOCK
|
|
{-# INLINE fromProtocolError #-}
|
|
|
|
checkCredentials _ (EntityId entId) cmd = case cmd of
|
|
-- IDTKN response must not have queue ID
|
|
NRTknId {} -> noEntity
|
|
-- IDSUB response must not have queue ID
|
|
NRSubId {} -> noEntity
|
|
-- ERR response does not always have entity ID
|
|
NRErr _ -> Right cmd
|
|
-- PONG response must not have queue ID
|
|
NRPong -> noEntity
|
|
-- other server responses must have entity ID
|
|
_
|
|
| B.null entId -> Left $ CMD NO_ENTITY
|
|
| otherwise -> Right cmd
|
|
where
|
|
noEntity
|
|
| B.null entId = Right cmd
|
|
| otherwise = Left $ CMD HAS_AUTH
|
|
|
|
data SMPQueueNtf = SMPQueueNtf
|
|
{ smpServer :: SMPServer,
|
|
notifierId :: NotifierId
|
|
}
|
|
deriving (Eq, Ord, Show)
|
|
|
|
instance Encoding SMPQueueNtf where
|
|
smpEncode SMPQueueNtf {smpServer, notifierId} = smpEncode (smpServer, notifierId)
|
|
smpP = do
|
|
smpServer <- updateSMPServerHosts <$> smpP
|
|
notifierId <- smpP
|
|
pure SMPQueueNtf {smpServer, notifierId}
|
|
|
|
instance StrEncoding SMPQueueNtf where
|
|
strEncode SMPQueueNtf {smpServer, notifierId} = strEncode smpServer <> "/" <> strEncode notifierId
|
|
strP = do
|
|
smpServer <- updateSMPServerHosts <$> strP
|
|
notifierId <- A.char '/' *> strP
|
|
pure SMPQueueNtf {smpServer, notifierId}
|
|
|
|
data PushProvider
|
|
= PPApnsDev -- provider for Apple development environment
|
|
| PPApnsProd -- production environment, including TestFlight
|
|
| PPApnsTest -- used for tests, to use APNS mock server
|
|
| PPApnsNull -- used to test servers from the client - does not communicate with APNS
|
|
deriving (Eq, Ord, Show)
|
|
|
|
instance Encoding PushProvider where
|
|
smpEncode = \case
|
|
PPApnsDev -> "AD"
|
|
PPApnsProd -> "AP"
|
|
PPApnsTest -> "AT"
|
|
PPApnsNull -> "AN"
|
|
smpP =
|
|
A.take 2 >>= \case
|
|
"AD" -> pure PPApnsDev
|
|
"AP" -> pure PPApnsProd
|
|
"AT" -> pure PPApnsTest
|
|
"AN" -> pure PPApnsNull
|
|
_ -> fail "bad PushProvider"
|
|
|
|
instance StrEncoding PushProvider where
|
|
strEncode = \case
|
|
PPApnsDev -> "apns_dev"
|
|
PPApnsProd -> "apns_prod"
|
|
PPApnsTest -> "apns_test"
|
|
PPApnsNull -> "apns_null"
|
|
strP =
|
|
A.takeTill (== ' ') >>= \case
|
|
"apns_dev" -> pure PPApnsDev
|
|
"apns_prod" -> pure PPApnsProd
|
|
"apns_test" -> pure PPApnsTest
|
|
"apns_null" -> pure PPApnsNull
|
|
_ -> fail "bad PushProvider"
|
|
|
|
instance FromField PushProvider where fromField = fromTextField_ $ eitherToMaybe . strDecode . encodeUtf8
|
|
|
|
instance ToField PushProvider where toField = toField . decodeLatin1 . strEncode
|
|
|
|
data DeviceToken = DeviceToken PushProvider ByteString
|
|
deriving (Eq, Ord, Show)
|
|
|
|
instance Encoding DeviceToken where
|
|
smpEncode (DeviceToken p t) = smpEncode (p, t)
|
|
smpP = DeviceToken <$> smpP <*> smpP
|
|
|
|
instance StrEncoding DeviceToken where
|
|
strEncode (DeviceToken p t) = strEncode p <> " " <> t
|
|
strP = nullToken <|> hexToken
|
|
where
|
|
nullToken = "apns_null test_ntf_token" $> DeviceToken PPApnsNull "test_ntf_token"
|
|
hexToken = DeviceToken <$> strP <* A.space <*> hexStringP
|
|
hexStringP =
|
|
A.takeWhile (`B.elem` "0123456789abcdef") >>= \s ->
|
|
if even (B.length s) then pure s else fail "odd number of hex characters"
|
|
|
|
instance ToJSON DeviceToken where
|
|
toEncoding (DeviceToken pp t) = J.pairs $ "pushProvider" .= decodeLatin1 (strEncode pp) <> "token" .= decodeLatin1 t
|
|
toJSON (DeviceToken pp t) = J.object ["pushProvider" .= decodeLatin1 (strEncode pp), "token" .= decodeLatin1 t]
|
|
|
|
instance FromJSON DeviceToken where
|
|
parseJSON = J.withObject "DeviceToken" $ \o -> do
|
|
pp <- strDecode . encodeUtf8 <$?> o .: "pushProvider"
|
|
t <- encodeUtf8 <$> o .: "token"
|
|
pure $ DeviceToken pp t
|
|
|
|
-- List of PNMessageData uses semicolon-separated encoding instead of strEncode,
|
|
-- because strEncode of NonEmpty list uses comma for separator,
|
|
-- and encoding of PNMessageData's smpQueue has comma in list of hosts
|
|
encodePNMessages :: NonEmpty PNMessageData -> ByteString
|
|
encodePNMessages = B.intercalate ";" . map strEncode . L.toList
|
|
|
|
pnMessagesP :: A.Parser (NonEmpty PNMessageData)
|
|
pnMessagesP = L.fromList <$> strP `A.sepBy1` A.char ';'
|
|
|
|
data PNMessageData = PNMessageData
|
|
{ smpQueue :: SMPQueueNtf,
|
|
ntfTs :: SystemTime,
|
|
nmsgNonce :: C.CbNonce,
|
|
encNMsgMeta :: EncNMsgMeta
|
|
}
|
|
deriving (Show)
|
|
|
|
instance StrEncoding PNMessageData where
|
|
strEncode PNMessageData {smpQueue, ntfTs, nmsgNonce, encNMsgMeta} =
|
|
strEncode (smpQueue, ntfTs, nmsgNonce, encNMsgMeta)
|
|
strP = do
|
|
(smpQueue, ntfTs, nmsgNonce, encNMsgMeta) <- strP
|
|
pure PNMessageData {smpQueue, ntfTs, nmsgNonce, encNMsgMeta}
|
|
|
|
type NtfEntityId = EntityId
|
|
|
|
type NtfSubscriptionId = NtfEntityId
|
|
|
|
type NtfTokenId = NtfEntityId
|
|
|
|
data NtfSubStatus
|
|
= -- | state after SNEW
|
|
NSNew
|
|
| -- | pending connection/subscription to SMP server
|
|
NSPending
|
|
| -- | connected and subscribed to SMP server
|
|
NSActive
|
|
| -- | disconnected/unsubscribed from SMP server
|
|
NSInactive
|
|
| -- | END received
|
|
NSEnd
|
|
| -- | DELD received (connection was deleted)
|
|
NSDeleted
|
|
| -- | SMP AUTH error
|
|
NSAuth
|
|
| -- | SMP SERVICE error - rejected service signature on individual subscriptions
|
|
NSService
|
|
| -- | SMP error other than AUTH
|
|
NSErr ByteString
|
|
deriving (Eq, Ord, Show)
|
|
|
|
-- if these statuses change, the queue ID hashes for services need to be updated in a new migration (see m20250830_queue_ids_hash)
|
|
subscribeNtfStatuses :: [NtfSubStatus]
|
|
subscribeNtfStatuses = [NSNew, NSPending, NSActive, NSInactive]
|
|
|
|
instance Encoding NtfSubStatus where
|
|
smpEncode = \case
|
|
NSNew -> "NEW"
|
|
NSPending -> "PENDING" -- e.g. after SMP server disconnect/timeout while ntf server is retrying to connect
|
|
NSActive -> "ACTIVE"
|
|
NSInactive -> "INACTIVE"
|
|
NSEnd -> "END"
|
|
NSDeleted -> "DELETED"
|
|
NSAuth -> "AUTH"
|
|
NSService -> "SERVICE"
|
|
NSErr err -> "ERR " <> err
|
|
smpP =
|
|
A.takeTill (== ' ') >>= \case
|
|
"NEW" -> pure NSNew
|
|
"PENDING" -> pure NSPending
|
|
"ACTIVE" -> pure NSActive
|
|
"INACTIVE" -> pure NSInactive
|
|
"END" -> pure NSEnd
|
|
"DELETED" -> pure NSDeleted
|
|
"AUTH" -> pure NSAuth
|
|
"SERVICE" -> pure NSService
|
|
"ERR" -> NSErr <$> (A.space *> A.takeByteString)
|
|
_ -> fail "bad NtfSubStatus"
|
|
|
|
instance StrEncoding NtfSubStatus where
|
|
strEncode = smpEncode
|
|
{-# INLINE strEncode #-}
|
|
strP = smpP
|
|
{-# INLINE strP #-}
|
|
|
|
data NtfTknStatus
|
|
= -- | Token created in DB
|
|
NTNew
|
|
| -- | state after registration (TNEW)
|
|
NTRegistered
|
|
| -- | if initial notification failed (push provider error) or verification failed
|
|
NTInvalid (Maybe NTInvalidReason)
|
|
| -- | Token confirmed via notification (accepted by push provider or verification code received by client)
|
|
NTConfirmed
|
|
| -- | after successful verification (TVFY)
|
|
NTActive
|
|
| -- | after it is no longer valid (push provider error)
|
|
NTExpired
|
|
deriving (Eq, Show)
|
|
|
|
allowTokenVerification :: NtfTknStatus -> Bool
|
|
allowTokenVerification = \case
|
|
NTNew -> False
|
|
NTRegistered -> True
|
|
NTInvalid _ -> False
|
|
NTConfirmed -> True
|
|
NTActive -> True
|
|
NTExpired -> False
|
|
|
|
allowNtfSubCommands :: NtfTknStatus -> Bool
|
|
allowNtfSubCommands = \case
|
|
NTNew -> False
|
|
NTRegistered -> False
|
|
-- TODO we could have separate statuses to show whether it became invalid
|
|
-- after verification (allow commands) or before (do not allow)
|
|
NTInvalid _ -> True
|
|
NTConfirmed -> False
|
|
NTActive -> True
|
|
NTExpired -> True
|
|
|
|
instance Encoding NtfTknStatus where
|
|
smpEncode = \case
|
|
NTNew -> "NEW"
|
|
NTRegistered -> "REGISTERED"
|
|
NTInvalid r_ -> "INVALID" <> maybe "" (\r -> ',' `B.cons` strEncode r) r_
|
|
NTConfirmed -> "CONFIRMED"
|
|
NTActive -> "ACTIVE"
|
|
NTExpired -> "EXPIRED"
|
|
smpP =
|
|
A.takeTill (\c -> c == ' ' || c == ',') >>= \case
|
|
"NEW" -> pure NTNew
|
|
"REGISTERED" -> pure NTRegistered
|
|
"INVALID" -> NTInvalid <$> optional (A.char ',' *> strP)
|
|
"CONFIRMED" -> pure NTConfirmed
|
|
"ACTIVE" -> pure NTActive
|
|
"EXPIRED" -> pure NTExpired
|
|
_ -> fail "bad NtfTknStatus"
|
|
|
|
instance StrEncoding NTInvalidReason where
|
|
strEncode = smpEncode
|
|
strP = smpP
|
|
|
|
data NTInvalidReason = NTIRBadToken | NTIRTokenNotForTopic | NTIRExpiredToken | NTIRUnregistered
|
|
deriving (Eq, Show)
|
|
|
|
instance Encoding NTInvalidReason where
|
|
smpEncode = \case
|
|
NTIRBadToken -> "BAD"
|
|
NTIRTokenNotForTopic -> "TOPIC"
|
|
NTIRExpiredToken -> "EXPIRED"
|
|
NTIRUnregistered -> "UNREGISTERED"
|
|
smpP =
|
|
A.takeTill (== ' ') >>= \case
|
|
"BAD" -> pure NTIRBadToken
|
|
"TOPIC" -> pure NTIRTokenNotForTopic
|
|
"EXPIRED" -> pure NTIRExpiredToken
|
|
"UNREGISTERED" -> pure NTIRUnregistered
|
|
_ -> fail "bad NTInvalidReason"
|
|
|
|
instance StrEncoding NtfTknStatus where
|
|
strEncode = smpEncode
|
|
strP = smpP
|
|
|
|
instance FromField NtfTknStatus where fromField = fromTextField_ $ either (const Nothing) Just . smpDecode . encodeUtf8
|
|
|
|
instance ToField NtfTknStatus where toField = toField . decodeLatin1 . smpEncode
|
|
|
|
instance ToJSON NtfTknStatus where
|
|
toEncoding = JE.text . decodeLatin1 . smpEncode
|
|
toJSON = J.String . decodeLatin1 . smpEncode
|
|
|
|
instance FromJSON NtfTknStatus where
|
|
parseJSON = J.withText "NtfTknStatus" $ either fail pure . smpDecode . encodeUtf8
|
|
|
|
checkEntity :: forall t e e'. (NtfEntityI e, NtfEntityI e') => t e' -> Either String (t e)
|
|
checkEntity c = case testEquality (sNtfEntity @e) (sNtfEntity @e') of
|
|
Just Refl -> Right c
|
|
Nothing -> Left "bad command party"
|
|
|
|
checkEntity' :: forall t p p'. (NtfEntityI p, NtfEntityI p') => t p' -> Maybe (t p)
|
|
checkEntity' c = case testEquality (sNtfEntity @p) (sNtfEntity @p') of
|
|
Just Refl -> Just c
|
|
_ -> Nothing
|