mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-03-31 09:46:03 +00:00
311 lines
10 KiB
Haskell
311 lines
10 KiB
Haskell
{-# LANGUAGE DataKinds #-}
|
|
{-# LANGUAGE DeriveAnyClass #-}
|
|
{-# LANGUAGE DeriveGeneric #-}
|
|
{-# LANGUAGE DuplicateRecordFields #-}
|
|
{-# LANGUAGE GADTs #-}
|
|
{-# LANGUAGE KindSignatures #-}
|
|
{-# LANGUAGE LambdaCase #-}
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# LANGUAGE RankNTypes #-}
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
|
|
module Simplex.Chat.Protocol where
|
|
|
|
import Control.Monad ((<=<))
|
|
import Data.Aeson (FromJSON, ToJSON, (.:), (.:?), (.=))
|
|
import qualified Data.Aeson as J
|
|
import qualified Data.Aeson.Types as JT
|
|
import qualified Data.Attoparsec.ByteString.Char8 as A
|
|
import qualified Data.ByteString.Lazy.Char8 as LB
|
|
import qualified Data.HashMap.Strict as H
|
|
import Data.Text (Text)
|
|
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
|
|
import Database.SQLite.Simple.FromField (FromField (..))
|
|
import Database.SQLite.Simple.ToField (ToField (..))
|
|
import GHC.Generics (Generic)
|
|
import Simplex.Chat.Types
|
|
import Simplex.Chat.Util (eitherToMaybe)
|
|
import Simplex.Messaging.Agent.Store.SQLite (fromTextField_)
|
|
import Simplex.Messaging.Encoding.String
|
|
import Simplex.Messaging.Util ((<$?>))
|
|
|
|
data ConnectionEntity
|
|
= RcvDirectMsgConnection Connection (Maybe Contact)
|
|
| RcvGroupMsgConnection Connection GroupInfo GroupMember
|
|
| SndFileConnection Connection SndFileTransfer
|
|
| RcvFileConnection Connection RcvFileTransfer
|
|
| UserContactConnection Connection UserContact
|
|
deriving (Eq, Show)
|
|
|
|
fromConnection :: ConnectionEntity -> Connection
|
|
fromConnection = \case
|
|
RcvDirectMsgConnection conn _ -> conn
|
|
RcvGroupMsgConnection conn _ _ -> conn
|
|
SndFileConnection conn _ -> conn
|
|
RcvFileConnection conn _ -> conn
|
|
UserContactConnection conn _ -> conn
|
|
|
|
-- chat message is sent as JSON with these properties
|
|
data AppMessage = AppMessage
|
|
{ event :: Text,
|
|
params :: J.Object
|
|
}
|
|
deriving (Generic, FromJSON)
|
|
|
|
instance ToJSON AppMessage where toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
|
|
|
|
newtype ChatMessage = ChatMessage {chatMsgEvent :: ChatMsgEvent}
|
|
deriving (Eq, Show)
|
|
|
|
instance StrEncoding ChatMessage where
|
|
strEncode = LB.toStrict . J.encode . chatToAppMessage
|
|
strDecode = appToChatMessage <=< J.eitherDecodeStrict'
|
|
strP = strDecode <$?> A.takeByteString
|
|
|
|
data ChatMsgEvent
|
|
= XMsgNew MsgContent
|
|
| XFile FileInvitation
|
|
| XFileAcpt String
|
|
| XInfo Profile
|
|
| XContact Profile (Maybe MsgContent)
|
|
| XGrpInv GroupInvitation
|
|
| XGrpAcpt MemberId
|
|
| XGrpMemNew MemberInfo
|
|
| XGrpMemIntro MemberInfo
|
|
| XGrpMemInv MemberId IntroInvitation
|
|
| XGrpMemFwd MemberInfo IntroInvitation
|
|
| XGrpMemInfo MemberId Profile
|
|
| XGrpMemCon MemberId
|
|
| XGrpMemConAll MemberId
|
|
| XGrpMemDel MemberId
|
|
| XGrpLeave
|
|
| XGrpDel
|
|
| XInfoProbe Probe
|
|
| XInfoProbeCheck ProbeHash
|
|
| XInfoProbeOk Probe
|
|
| XOk
|
|
deriving (Eq, Show)
|
|
|
|
data MsgContentType = MCText_ | MCUnknown_
|
|
|
|
instance StrEncoding MsgContentType where
|
|
strEncode = \case
|
|
MCText_ -> "text"
|
|
MCUnknown_ -> "text"
|
|
strDecode = \case
|
|
"text" -> Right MCText_
|
|
_ -> Right MCUnknown_
|
|
strP = strDecode <$?> A.takeTill (== ' ')
|
|
|
|
instance FromJSON MsgContentType where
|
|
parseJSON = strParseJSON "MsgContentType"
|
|
|
|
instance ToJSON MsgContentType where
|
|
toJSON = strToJSON
|
|
toEncoding = strToJEncoding
|
|
|
|
-- TODO - include tag and original JSON into MCUnknown so that information is not lost
|
|
-- so when it serializes back it is the same as it was and chat upgrade makes it readable
|
|
data MsgContent = MCText Text | MCUnknown
|
|
deriving (Eq, Show)
|
|
|
|
msgContentText :: MsgContent -> Text
|
|
msgContentText = \case
|
|
MCText t -> t
|
|
MCUnknown -> unknownMsgType
|
|
|
|
toMsgContentType :: MsgContent -> MsgContentType
|
|
toMsgContentType = \case
|
|
MCText _ -> MCText_
|
|
MCUnknown -> MCUnknown_
|
|
|
|
instance FromJSON MsgContent where
|
|
parseJSON (J.Object v) = do
|
|
v .: "type" >>= \case
|
|
MCText_ -> MCText <$> v .: "text"
|
|
MCUnknown_ -> pure MCUnknown
|
|
parseJSON invalid =
|
|
JT.prependFailure "bad MsgContent, " (JT.typeMismatch "Object" invalid)
|
|
|
|
unknownMsgType :: Text
|
|
unknownMsgType = "unknown message type"
|
|
|
|
instance ToJSON MsgContent where
|
|
toJSON mc =
|
|
J.object $
|
|
("type" .= toMsgContentType mc) : case mc of
|
|
MCText t -> ["text" .= t]
|
|
MCUnknown -> ["text" .= unknownMsgType]
|
|
toEncoding mc =
|
|
J.pairs $
|
|
("type" .= toMsgContentType mc) <> case mc of
|
|
MCText t -> "text" .= t
|
|
MCUnknown -> "text" .= unknownMsgType
|
|
|
|
data CMEventTag
|
|
= XMsgNew_
|
|
| XFile_
|
|
| XFileAcpt_
|
|
| XInfo_
|
|
| XContact_
|
|
| XGrpInv_
|
|
| XGrpAcpt_
|
|
| XGrpMemNew_
|
|
| XGrpMemIntro_
|
|
| XGrpMemInv_
|
|
| XGrpMemFwd_
|
|
| XGrpMemInfo_
|
|
| XGrpMemCon_
|
|
| XGrpMemConAll_
|
|
| XGrpMemDel_
|
|
| XGrpLeave_
|
|
| XGrpDel_
|
|
| XInfoProbe_
|
|
| XInfoProbeCheck_
|
|
| XInfoProbeOk_
|
|
| XOk_
|
|
deriving (Eq, Show)
|
|
|
|
instance StrEncoding CMEventTag where
|
|
strEncode = \case
|
|
XMsgNew_ -> "x.msg.new"
|
|
XFile_ -> "x.file"
|
|
XFileAcpt_ -> "x.file.acpt"
|
|
XInfo_ -> "x.info"
|
|
XContact_ -> "x.contact"
|
|
XGrpInv_ -> "x.grp.inv"
|
|
XGrpAcpt_ -> "x.grp.acpt"
|
|
XGrpMemNew_ -> "x.grp.mem.new"
|
|
XGrpMemIntro_ -> "x.grp.mem.intro"
|
|
XGrpMemInv_ -> "x.grp.mem.inv"
|
|
XGrpMemFwd_ -> "x.grp.mem.fwd"
|
|
XGrpMemInfo_ -> "x.grp.mem.info"
|
|
XGrpMemCon_ -> "x.grp.mem.con"
|
|
XGrpMemConAll_ -> "x.grp.mem.con.all"
|
|
XGrpMemDel_ -> "x.grp.mem.del"
|
|
XGrpLeave_ -> "x.grp.leave"
|
|
XGrpDel_ -> "x.grp.del"
|
|
XInfoProbe_ -> "x.info.probe"
|
|
XInfoProbeCheck_ -> "x.info.probe.check"
|
|
XInfoProbeOk_ -> "x.info.probe.ok"
|
|
XOk_ -> "x.ok"
|
|
strDecode = \case
|
|
"x.msg.new" -> Right XMsgNew_
|
|
"x.file" -> Right XFile_
|
|
"x.file.acpt" -> Right XFileAcpt_
|
|
"x.info" -> Right XInfo_
|
|
"x.contact" -> Right XContact_
|
|
"x.grp.inv" -> Right XGrpInv_
|
|
"x.grp.acpt" -> Right XGrpAcpt_
|
|
"x.grp.mem.new" -> Right XGrpMemNew_
|
|
"x.grp.mem.intro" -> Right XGrpMemIntro_
|
|
"x.grp.mem.inv" -> Right XGrpMemInv_
|
|
"x.grp.mem.fwd" -> Right XGrpMemFwd_
|
|
"x.grp.mem.info" -> Right XGrpMemInfo_
|
|
"x.grp.mem.con" -> Right XGrpMemCon_
|
|
"x.grp.mem.con.all" -> Right XGrpMemConAll_
|
|
"x.grp.mem.del" -> Right XGrpMemDel_
|
|
"x.grp.leave" -> Right XGrpLeave_
|
|
"x.grp.del" -> Right XGrpDel_
|
|
"x.info.probe" -> Right XInfoProbe_
|
|
"x.info.probe.check" -> Right XInfoProbeCheck_
|
|
"x.info.probe.ok" -> Right XInfoProbeOk_
|
|
"x.ok" -> Right XOk_
|
|
_ -> Left "bad CMEventTag"
|
|
strP = strDecode <$?> A.takeTill (== ' ')
|
|
|
|
toCMEventTag :: ChatMsgEvent -> CMEventTag
|
|
toCMEventTag = \case
|
|
XMsgNew _ -> XMsgNew_
|
|
XFile _ -> XFile_
|
|
XFileAcpt _ -> XFileAcpt_
|
|
XInfo _ -> XInfo_
|
|
XContact _ _ -> XContact_
|
|
XGrpInv _ -> XGrpInv_
|
|
XGrpAcpt _ -> XGrpAcpt_
|
|
XGrpMemNew _ -> XGrpMemNew_
|
|
XGrpMemIntro _ -> XGrpMemIntro_
|
|
XGrpMemInv _ _ -> XGrpMemInv_
|
|
XGrpMemFwd _ _ -> XGrpMemFwd_
|
|
XGrpMemInfo _ _ -> XGrpMemInfo_
|
|
XGrpMemCon _ -> XGrpMemCon_
|
|
XGrpMemConAll _ -> XGrpMemConAll_
|
|
XGrpMemDel _ -> XGrpMemDel_
|
|
XGrpLeave -> XGrpLeave_
|
|
XGrpDel -> XGrpDel_
|
|
XInfoProbe _ -> XInfoProbe_
|
|
XInfoProbeCheck _ -> XInfoProbeCheck_
|
|
XInfoProbeOk _ -> XInfoProbeOk_
|
|
XOk -> XOk_
|
|
|
|
cmEventTagT :: Text -> Maybe CMEventTag
|
|
cmEventTagT = eitherToMaybe . strDecode . encodeUtf8
|
|
|
|
serializeCMEventTag :: CMEventTag -> Text
|
|
serializeCMEventTag = decodeLatin1 . strEncode
|
|
|
|
instance FromField CMEventTag where fromField = fromTextField_ cmEventTagT
|
|
|
|
instance ToField CMEventTag where toField = toField . serializeCMEventTag
|
|
|
|
appToChatMessage :: AppMessage -> Either String ChatMessage
|
|
appToChatMessage AppMessage {event, params} = do
|
|
eventTag <- strDecode $ encodeUtf8 event
|
|
chatMsgEvent <- msg eventTag
|
|
pure ChatMessage {chatMsgEvent}
|
|
where
|
|
p :: FromJSON a => Text -> Either String a
|
|
p key = JT.parseEither (.: key) params
|
|
msg = \case
|
|
XMsgNew_ -> XMsgNew <$> p "content"
|
|
XFile_ -> XFile <$> p "file"
|
|
XFileAcpt_ -> XFileAcpt <$> p "fileName"
|
|
XInfo_ -> XInfo <$> p "profile"
|
|
XContact_ -> XContact <$> p "profile" <*> JT.parseEither (.:? "content") params
|
|
XGrpInv_ -> XGrpInv <$> p "groupInvitation"
|
|
XGrpAcpt_ -> XGrpAcpt <$> p "memberId"
|
|
XGrpMemNew_ -> XGrpMemNew <$> p "memberInfo"
|
|
XGrpMemIntro_ -> XGrpMemIntro <$> p "memberInfo"
|
|
XGrpMemInv_ -> XGrpMemInv <$> p "memberId" <*> p "memberIntro"
|
|
XGrpMemFwd_ -> XGrpMemFwd <$> p "memberInfo" <*> p "memberIntro"
|
|
XGrpMemInfo_ -> XGrpMemInfo <$> p "memberId" <*> p "profile"
|
|
XGrpMemCon_ -> XGrpMemCon <$> p "memberId"
|
|
XGrpMemConAll_ -> XGrpMemConAll <$> p "memberId"
|
|
XGrpMemDel_ -> XGrpMemDel <$> p "memberId"
|
|
XGrpLeave_ -> pure XGrpLeave
|
|
XGrpDel_ -> pure XGrpDel
|
|
XInfoProbe_ -> XInfoProbe <$> p "probe"
|
|
XInfoProbeCheck_ -> XInfoProbeCheck <$> p "probeHash"
|
|
XInfoProbeOk_ -> XInfoProbeOk <$> p "probe"
|
|
XOk_ -> pure XOk
|
|
|
|
chatToAppMessage :: ChatMessage -> AppMessage
|
|
chatToAppMessage ChatMessage {chatMsgEvent} = AppMessage {event, params}
|
|
where
|
|
event = serializeCMEventTag . toCMEventTag $ chatMsgEvent
|
|
o :: [(Text, J.Value)] -> J.Object
|
|
o = H.fromList
|
|
params = case chatMsgEvent of
|
|
XMsgNew content -> o ["content" .= content]
|
|
XFile fileInv -> o ["file" .= fileInv]
|
|
XFileAcpt fileName -> o ["fileName" .= fileName]
|
|
XInfo profile -> o ["profile" .= profile]
|
|
XContact profile content -> o $ maybe id ((:) . ("content" .=)) content ["profile" .= profile]
|
|
XGrpInv groupInv -> o ["groupInvitation" .= groupInv]
|
|
XGrpAcpt memId -> o ["memberId" .= memId]
|
|
XGrpMemNew memInfo -> o ["memberInfo" .= memInfo]
|
|
XGrpMemIntro memInfo -> o ["memberInfo" .= memInfo]
|
|
XGrpMemInv memId memIntro -> o ["memberId" .= memId, "memberIntro" .= memIntro]
|
|
XGrpMemFwd memInfo memIntro -> o ["memberInfo" .= memInfo, "memberIntro" .= memIntro]
|
|
XGrpMemInfo memId profile -> o ["memberId" .= memId, "profile" .= profile]
|
|
XGrpMemCon memId -> o ["memberId" .= memId]
|
|
XGrpMemConAll memId -> o ["memberId" .= memId]
|
|
XGrpMemDel memId -> o ["memberId" .= memId]
|
|
XGrpLeave -> H.empty
|
|
XGrpDel -> H.empty
|
|
XInfoProbe probe -> o ["probe" .= probe]
|
|
XInfoProbeCheck probeHash -> o ["probeHash" .= probeHash]
|
|
XInfoProbeOk probe -> o ["probe" .= probe]
|
|
XOk -> H.empty
|