{-# OPTIONS_HADDOCK not-home #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module MagicWormhole.Internal.Messages
( ClientMessage(..)
, ServerMessage(..)
, AppID(..)
, MailboxMessage(..)
, WelcomeMessage(..)
, MessageID(..)
, Side(..)
, generateSide
, Phase(..)
, phaseName
, Body(..)
, Nameplate(..)
, Mailbox(..)
, Mood(..)
) where
import Protolude
import Control.Monad (fail)
import Crypto.Random (MonadRandom(..))
import Data.Aeson
( FromJSON(..)
, ToJSON(..)
, Value(Object, String)
, (.:)
, (.:?)
, (.=)
, object
)
import Data.Aeson.Types (Pair, typeMismatch)
import Data.ByteArray.Encoding (convertFromBase, convertToBase, Base(Base16))
import Numeric (readHex, showHex)
data ServerMessage
=
Welcome WelcomeMessage
|
Nameplates
{
nameplates :: [Nameplate]
}
|
Allocated
{
nameplate :: Nameplate
}
|
Claimed
{
mailbox :: Mailbox
}
|
Released
|
Message MailboxMessage
|
Closed
|
Ack
|
Pong Int
|
Error
{
errorMessage :: Text
, original :: ClientMessage
}
deriving (Eq, Show)
instance FromJSON ServerMessage where
parseJSON (Object v) = do
t <- v .: "type"
case t of
"welcome" -> do
welcome <- v .: "welcome"
Welcome <$> (WelcomeMessage <$> welcome .:? "motd" <*> welcome .:? "error")
"nameplates" -> do
ns <- v .: "nameplates"
Nameplates <$> sequence [ Nameplate <$> n .: "id" | n <- ns ]
"allocated" -> Allocated <$> v .: "nameplate"
"claimed" -> Claimed <$> v .: "mailbox"
"released" -> pure Released
"message" -> Message <$> (MailboxMessage <$> v .: "side" <*> v .: "phase" <*> v .:? "id" <*> v .: "body")
"closed" -> pure Closed
"ack" -> pure Ack
"pong" -> Pong <$> v .: "pong"
"error" -> Error <$> v .: "error" <*> v .: "orig"
_ -> fail $ "Unrecognized wormhole message type: " <> t
parseJSON unknown = typeMismatch "Message" unknown
instance ToJSON ServerMessage where
toJSON (Welcome (WelcomeMessage motd' error')) =
objectWithType "welcome"
[ "welcome" .= object (catMaybes [ ("motd" .=) <$> motd'
, ("error" .=) <$> error'
])
]
toJSON (Nameplates nameplates') =
objectWithType "nameplates" ["nameplates" .= [ object ["id" .= n] | n <- nameplates' ] ]
toJSON (Allocated nameplate') =
objectWithType "allocated" [ "nameplate" .= nameplate' ]
toJSON (Claimed mailbox') =
objectWithType "claimed" [ "mailbox" .= mailbox' ]
toJSON Released = objectWithType "released" []
toJSON (Message (MailboxMessage side' phase' id body')) =
objectWithType "message"
[ "phase" .= phase'
, "side" .= side'
, "body" .= body'
, "id" .= id
]
toJSON Closed = objectWithType "closed" []
toJSON Ack = objectWithType "ack" []
toJSON (Pong n) = objectWithType "pong" ["pong" .= n]
toJSON (Error errorMsg orig) =
objectWithType "error" [ "error" .= errorMsg
, "orig" .= orig
]
objectWithType :: Text -> [Pair] -> Value
objectWithType typ pairs = object $ ("type" .= typ):pairs
newtype Nameplate = Nameplate Text deriving (Eq, Show, ToJSON, FromJSON)
data Phase
=
PakePhase
|
VersionPhase
| ApplicationPhase Int
deriving (Eq, Show)
phaseName :: Phase -> Text
phaseName PakePhase = "pake"
phaseName VersionPhase = "version"
phaseName (ApplicationPhase n) = show n
instance ToJSON Phase where
toJSON = toJSON . phaseName
instance FromJSON Phase where
parseJSON (String "pake") = pure PakePhase
parseJSON (String "version") = pure VersionPhase
parseJSON (String number) =
let number' = toS number in
case readMaybe number' of
Just n -> pure (ApplicationPhase n)
Nothing -> fail $ "Unrecognized phase: " <> number'
parseJSON other = typeMismatch "Phase" other
newtype Mailbox = Mailbox Text deriving (Eq, Show, ToJSON, FromJSON)
newtype Body = Body ByteString deriving (Eq, Show)
instance ToJSON Body where
toJSON (Body bytes) = toJSON (toS @ByteString @Text (convertToBase Base16 bytes))
instance FromJSON Body where
parseJSON (String s) = either fail (pure . Body) (convertFromBase Base16 (toS @Text @ByteString s))
parseJSON x = typeMismatch "Body" x
data ClientMessage
=
Bind AppID Side
| List
| Allocate
| Claim Nameplate
| Release (Maybe Nameplate)
| Open Mailbox
| Add Phase Body
| Close (Maybe Mailbox) (Maybe Mood)
| Ping Int
deriving (Eq, Show)
instance FromJSON ClientMessage where
parseJSON (Object v) = do
t <- v .: "type"
case t of
"bind" -> Bind <$> v .: "appid" <*> v .: "side"
"list" -> pure List
"allocate" -> pure Allocate
"claim" -> Claim <$> v .: "nameplate"
"release" -> Release <$> v .:? "nameplate"
"open" -> Open <$> v .: "mailbox"
"add" -> Add <$> v .: "phase" <*> v .: "body"
"close" -> Close <$> v .:? "mailbox" <*> v .:? "mood"
"ping" -> Ping <$> v .: "ping"
_ -> fail $ "Unrecognized rendezvous client message type: " <> t
parseJSON unknown = typeMismatch "Message" unknown
instance ToJSON ClientMessage where
toJSON (Bind appID side') =
objectWithType "bind" [ "appid" .= appID
, "side" .= side'
]
toJSON List = objectWithType "list" []
toJSON Allocate = objectWithType "allocate" []
toJSON (Claim nameplate') = objectWithType "claim" [ "nameplate" .= nameplate' ]
toJSON (Release nameplate') =
objectWithType "release" $ case nameplate' of
Nothing -> []
Just n -> ["nameplate" .= n]
toJSON (Open mailbox') = objectWithType "open" [ "mailbox" .= mailbox' ]
toJSON (Add phase' body') = objectWithType "add"
[ "phase" .= phase'
, "body" .= body'
]
toJSON (Close mailbox' mood') =
objectWithType "close" $ catMaybes [ ("mailbox" .=) <$> mailbox'
, ("mood" .=) <$> mood'
]
toJSON (Ping n) = objectWithType "ping" [ "ping" .= n]
newtype AppID = AppID Text deriving (Eq, Show, FromJSON, ToJSON)
newtype Side = Side Text deriving (Eq, Show, FromJSON, ToJSON)
generateSide :: MonadRandom randomly => randomly Side
generateSide = do
randomBytes <- getRandomBytes 5
pure . Side . toS @ByteString . convertToBase Base16 $ (randomBytes :: ByteString)
data Mood
=
Happy
| Lonely
| Scary
| Errory deriving (Eq, Show)
instance ToJSON Mood where
toJSON Happy = "happy"
toJSON Lonely = "lonely"
toJSON Scary = "scary"
toJSON Errory = "errory"
instance FromJSON Mood where
parseJSON (String s) =
case s of
"happy" -> pure Happy
"lonely" -> pure Lonely
"scary" -> pure Scary
"errory" -> pure Errory
_ -> fail $ "Unrecognized mood: " <> toS s
parseJSON unknown = typeMismatch "Mood" unknown
newtype MessageID = MessageID Int16 deriving (Eq, Show, Hashable)
instance ToJSON MessageID where
toJSON (MessageID n) = toJSON $ showHex n ""
instance FromJSON MessageID where
parseJSON (String s) =
case readHex (toS s) of
[(n, _)] -> pure (MessageID n)
_ -> fail $ "Could not parse MessageID: " <> toS s
parseJSON unknown = typeMismatch "MessageID" unknown
data MailboxMessage
= MailboxMessage
{
side :: Side
,
phase :: Phase
, messageID :: Maybe MessageID
,
body :: Body
} deriving (Eq, Show)
data WelcomeMessage
= WelcomeMessage
{
motd :: Maybe Text
, welcomeErrorMessage :: Maybe Text
} deriving (Eq, Show)