Files
simplex-chat/src/Simplex/Chat/Messages.hs
Evgeny Poberezkin b2f9ee4326 Merge branch 'stable'
2025-07-25 21:16:32 +01:00

1595 lines
57 KiB
Haskell

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
{-# OPTIONS_GHC -fno-warn-operator-whitespace #-}
module Simplex.Chat.Messages where
import Control.Applicative ((<|>))
import Control.Monad ((>=>))
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 qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Char (isSpace)
import Data.Int (Int64)
import Data.Kind (Constraint)
import Data.List.NonEmpty (NonEmpty)
import Data.Map.Strict (Map)
import Data.Maybe (fromMaybe, isJust, isNothing)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
import Data.Time.Clock (NominalDiffTime, UTCTime, diffUTCTime, nominalDay)
import Data.Type.Equality
import Data.Typeable (Typeable)
import GHC.TypeLits (ErrorMessage (ShowType, type (:<>:)), TypeError)
import qualified GHC.TypeLits as Type
import Simplex.Chat.Markdown
import Simplex.Chat.Messages.CIContent
import Simplex.Chat.Options.DB (FromField (..), ToField (..))
import Simplex.Chat.Protocol
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.Util (textParseJSON)
import Simplex.Messaging.Agent.Protocol (AgentMsgId, MsgMeta (..), MsgReceiptStatus (..))
import Simplex.Messaging.Agent.Store.DB (fromTextField_)
import Simplex.Messaging.Crypto.File (CryptoFile (..))
import qualified Simplex.Messaging.Crypto.File as CF
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, sumTypeJSON)
import Simplex.Messaging.Protocol (BlockingInfo, MsgBody)
import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, (<$?>))
data ChatType = CTDirect | CTGroup | CTLocal | CTContactRequest | CTContactConnection
deriving (Eq, Show, Ord)
$(JQ.deriveJSON (enumJSON $ dropPrefix "CT") ''ChatType)
data SChatType (c :: ChatType) where
SCTDirect :: SChatType 'CTDirect
SCTGroup :: SChatType 'CTGroup
SCTLocal :: SChatType 'CTLocal
SCTContactRequest :: SChatType 'CTContactRequest
SCTContactConnection :: SChatType 'CTContactConnection
deriving instance Show (SChatType c)
instance TestEquality SChatType where
testEquality SCTDirect SCTDirect = Just Refl
testEquality SCTGroup SCTGroup = Just Refl
testEquality SCTLocal SCTLocal = Just Refl
testEquality SCTContactRequest SCTContactRequest = Just Refl
testEquality SCTContactConnection SCTContactConnection = Just Refl
testEquality _ _ = Nothing
data AChatType = forall c. ChatTypeI c => ACT (SChatType c)
class ChatTypeI (c :: ChatType) where
chatTypeI :: SChatType c
instance ChatTypeI 'CTDirect where chatTypeI = SCTDirect
instance ChatTypeI 'CTGroup where chatTypeI = SCTGroup
instance ChatTypeI 'CTLocal where chatTypeI = SCTLocal
instance ChatTypeI 'CTContactRequest where chatTypeI = SCTContactRequest
instance ChatTypeI 'CTContactConnection where chatTypeI = SCTContactConnection
toChatType :: SChatType c -> ChatType
toChatType = \case
SCTDirect -> CTDirect
SCTGroup -> CTGroup
SCTLocal -> CTLocal
SCTContactRequest -> CTContactRequest
SCTContactConnection -> CTContactConnection
aChatType :: ChatType -> AChatType
aChatType = \case
CTDirect -> ACT SCTDirect
CTGroup -> ACT SCTGroup
CTLocal -> ACT SCTLocal
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 GroupChatScope
= GCSMemberSupport {groupMemberId_ :: Maybe GroupMemberId} -- Nothing means own conversation with support
deriving (Eq, Show, Ord)
data GroupChatScopeTag
= GCSTMemberSupport_
deriving (Eq, Show)
instance FromField GroupChatScopeTag where fromField = fromTextField_ textDecode
instance ToField GroupChatScopeTag where toField = toField . textEncode
instance TextEncoding GroupChatScopeTag where
textDecode = \case
"member_support" -> Just GCSTMemberSupport_
_ -> Nothing
textEncode = \case
GCSTMemberSupport_ -> "member_support"
data ChatName = ChatName {chatType :: ChatType, chatName :: Text}
deriving (Show)
data SendName
= SNDirect ContactName
| SNGroup GroupName (Maybe GroupScopeName)
| SNLocal
deriving (Show)
data GroupScopeName
= GSNMemberSupport (Maybe ContactName)
deriving (Show)
chatTypeStr :: ChatType -> Text
chatTypeStr = \case
CTDirect -> "@"
CTGroup -> "#"
CTLocal -> "*"
CTContactRequest -> "<@"
CTContactConnection -> ":"
chatNameStr :: ChatName -> String
chatNameStr (ChatName cType name) = T.unpack $ chatTypeStr cType <> if T.any isSpace name then "'" <> name <> "'" else name
data ChatRef = ChatRef {chatType :: ChatType, chatId :: Int64, chatScope :: Maybe GroupChatScope}
deriving (Eq, Show, Ord)
data ChatInfo (c :: ChatType) where
DirectChat :: Contact -> ChatInfo 'CTDirect
GroupChat :: GroupInfo -> Maybe GroupChatScopeInfo -> ChatInfo 'CTGroup
LocalChat :: NoteFolder -> ChatInfo 'CTLocal
ContactRequest :: UserContactRequest -> ChatInfo 'CTContactRequest
ContactConnection :: PendingContactConnection -> ChatInfo 'CTContactConnection
CInfoInvalidJSON :: SChatType c -> J.Object -> ChatInfo c -- this constructor is needed to catch JSON errors for Remote connection parsing
deriving instance Show (ChatInfo c)
data GroupChatScopeInfo
= GCSIMemberSupport {groupMember_ :: Maybe GroupMember}
deriving (Show)
toChatScope :: GroupChatScopeInfo -> GroupChatScope
toChatScope = \case
GCSIMemberSupport {groupMember_} -> GCSMemberSupport $ groupMemberId' <$> groupMember_
toMsgScope :: GroupInfo -> GroupChatScopeInfo -> MsgScope
toMsgScope GroupInfo {membership} = \case
GCSIMemberSupport {groupMember_} -> MSMember $ memberId' $ fromMaybe membership groupMember_
data GroupForwardScope
= GFSAll -- message should be forwarded to all group members, even pending (e.g. XGrpDel, XGrpInfo)
| GFSMain -- message should be forwarded to current group members only (e.g. regular messages in group)
| GFSMemberSupport GroupMemberId
deriving (Eq, Ord, Show)
toGroupForwardScope :: GroupInfo -> Maybe GroupChatScopeInfo -> GroupForwardScope
toGroupForwardScope GroupInfo {membership} = \case
Nothing -> GFSMain
Just GCSIMemberSupport {groupMember_} -> GFSMemberSupport $ groupMemberId' $ fromMaybe membership groupMember_
memberEventForwardScope :: GroupMember -> Maybe GroupForwardScope
memberEventForwardScope m@GroupMember {memberRole, memberStatus}
| memberStatus == GSMemPendingApproval = Nothing
| memberStatus == GSMemPendingReview = Just $ GFSMemberSupport $ groupMemberId' m
| memberRole >= GRModerator = Just GFSAll
| otherwise = Just GFSMain
chatInfoToRef :: ChatInfo c -> Maybe ChatRef
chatInfoToRef = \case
DirectChat Contact {contactId} -> Just $ ChatRef CTDirect contactId Nothing
GroupChat GroupInfo {groupId} scopeInfo -> Just $ ChatRef CTGroup groupId (toChatScope <$> scopeInfo)
LocalChat NoteFolder {noteFolderId} -> Just $ ChatRef CTLocal noteFolderId Nothing
ContactRequest UserContactRequest {contactRequestId} -> Just $ ChatRef CTContactRequest contactRequestId Nothing
ContactConnection PendingContactConnection {pccConnId} -> Just $ ChatRef CTContactConnection pccConnId Nothing
CInfoInvalidJSON {} -> Nothing
chatInfoMembership :: ChatInfo c -> Maybe GroupMember
chatInfoMembership = \case
GroupChat GroupInfo {membership} _scopeInfo -> Just membership
_ -> Nothing
data JSONChatInfo
= JCInfoDirect {contact :: Contact}
| JCInfoGroup {groupInfo :: GroupInfo, groupChatScope :: Maybe GroupChatScopeInfo}
| JCInfoLocal {noteFolder :: NoteFolder}
| JCInfoContactRequest {contactRequest :: UserContactRequest}
| JCInfoContactConnection {contactConnection :: PendingContactConnection}
| JCInfoInvalidJSON {chatType :: ChatType, json :: J.Object}
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "GCSI") ''GroupChatScopeInfo)
$(JQ.deriveToJSON (sumTypeJSON $ dropPrefix "JCInfo") ''JSONChatInfo)
instance FromJSON JSONChatInfo where
parseJSON v@(J.Object o) =
$(JQ.mkParseJSON (sumTypeJSON $ dropPrefix "JCInfo") ''JSONChatInfo) v
<|> ((`JCInfoInvalidJSON` o) <$> o .: "type") -- fallback for forward compatible remote parser
parseJSON invalid = JT.typeMismatch "Object" invalid
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
jsonChatInfo :: ChatInfo c -> JSONChatInfo
jsonChatInfo = \case
DirectChat c -> JCInfoDirect c
GroupChat g s -> JCInfoGroup g s
LocalChat l -> JCInfoLocal l
ContactRequest g -> JCInfoContactRequest g
ContactConnection c -> JCInfoContactConnection c
CInfoInvalidJSON c o -> JCInfoInvalidJSON (toChatType c) o
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 s -> AChatInfo SCTGroup $ GroupChat g s
JCInfoLocal l -> AChatInfo SCTLocal $ LocalChat l
JCInfoContactRequest g -> AChatInfo SCTContactRequest $ ContactRequest g
JCInfoContactConnection c -> AChatInfo SCTContactConnection $ ContactConnection c
JCInfoInvalidJSON cType o -> case aChatType cType of ACT c -> AChatInfo c $ CInfoInvalidJSON c o
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
data ChatItem (c :: ChatType) (d :: MsgDirection) = ChatItem
{ chatDir :: CIDirection c d,
meta :: CIMeta c d,
content :: CIContent d,
-- The `mentions` map prevents loading all members from UI.
-- The key is a name used in the message text, used to look up CIMention.
mentions :: Map MemberName CIMention,
formattedText :: Maybe MarkdownList,
quotedItem :: Maybe (CIQuote c),
reactions :: [CIReactionCount],
file :: Maybe (CIFile d)
}
deriving (Show)
data CIMention = CIMention
{ memberId :: MemberId,
-- member record can be created later than the mention is received
memberRef :: Maybe CIMentionMember
}
deriving (Eq, Show)
data CIMentionMember = CIMentionMember
{ groupMemberId :: GroupMemberId,
displayName :: Text, -- use `displayName` in copy/share actions
localAlias :: Maybe Text, -- use `fromMaybe displayName localAlias` in chat view
memberRole :: GroupMemberRole -- shown for admins/owners in the message
}
deriving (Eq, Show)
isACIUserMention :: AChatItem -> Bool
isACIUserMention (AChatItem _ _ _ ci) = isUserMention ci
isUserMention :: ChatItem c d -> Bool
isUserMention ChatItem {meta = CIMeta {userMention}} = userMention
data CIDirection (c :: ChatType) (d :: MsgDirection) where
CIDirectSnd :: CIDirection 'CTDirect 'MDSnd
CIDirectRcv :: CIDirection 'CTDirect 'MDRcv
CIGroupSnd :: CIDirection 'CTGroup 'MDSnd
CIGroupRcv :: GroupMember -> CIDirection 'CTGroup 'MDRcv
CILocalSnd :: CIDirection 'CTLocal 'MDSnd
CILocalRcv :: CIDirection 'CTLocal 'MDRcv
deriving instance Show (CIDirection c d)
data CCIDirection c = forall d. MsgDirectionI d => CCID (SMsgDirection d) (CIDirection c d)
data ACIDirection = forall c d. (ChatTypeI c, MsgDirectionI d) => ACID (SChatType c) (SMsgDirection d) (CIDirection c d)
data JSONCIDirection
= JCIDirectSnd
| JCIDirectRcv
| JCIGroupSnd
| JCIGroupRcv {groupMember :: GroupMember}
| JCILocalSnd
| JCILocalRcv
deriving (Show)
jsonCIDirection :: CIDirection c d -> JSONCIDirection
jsonCIDirection = \case
CIDirectSnd -> JCIDirectSnd
CIDirectRcv -> JCIDirectRcv
CIGroupSnd -> JCIGroupSnd
CIGroupRcv m -> JCIGroupRcv m
CILocalSnd -> JCILocalSnd
CILocalRcv -> JCILocalRcv
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
JCILocalSnd -> ACID SCTLocal SMDSnd CILocalSnd
JCILocalRcv -> ACID SCTLocal SMDRcv CILocalRcv
data CIReactionCount = CIReactionCount {reaction :: MsgReaction, userReacted :: Bool, totalReacted :: Int}
deriving (Show)
data CChatItem c = forall d. MsgDirectionI d => CChatItem (SMsgDirection d) (ChatItem c d)
deriving instance Show (CChatItem c)
cChatItemId :: CChatItem c -> ChatItemId
cChatItemId (CChatItem _ ci) = chatItemId' ci
chatItemId' :: ChatItem c d -> ChatItemId
chatItemId' ChatItem {meta = CIMeta {itemId}} = itemId
chatItemTs :: CChatItem c -> UTCTime
chatItemTs (CChatItem _ ci) = chatItemTs' ci
chatItemTs' :: ChatItem c d -> UTCTime
chatItemTs' ChatItem {meta = CIMeta {itemTs}} = itemTs
ciCreatedAt :: CChatItem c -> UTCTime
ciCreatedAt (CChatItem _ ci) = ciCreatedAt' ci
ciCreatedAt' :: ChatItem c d -> UTCTime
ciCreatedAt' ChatItem {meta = CIMeta {createdAt}} = createdAt
chatItemTimed :: ChatItem c d -> Maybe CITimed
chatItemTimed ChatItem {meta = CIMeta {itemTimed}} = itemTimed
timedDeleteAt' :: CITimed -> Maybe UTCTime
timedDeleteAt' CITimed {deleteAt} = deleteAt
chatItemMember :: GroupInfo -> ChatItem 'CTGroup d -> GroupMember
chatItemMember GroupInfo {membership} ChatItem {chatDir} = case chatDir of
CIGroupSnd -> membership
CIGroupRcv m -> m
chatItemRcvFromMember :: ChatItem c d -> Maybe GroupMember
chatItemRcvFromMember ChatItem {chatDir} = case chatDir of
CIGroupRcv m -> Just m
_ -> Nothing
chatItemIsRcvNew :: ChatItem c d -> Bool
chatItemIsRcvNew ChatItem {meta = CIMeta {itemStatus}} = case itemStatus of
CISRcvNew -> True
_ -> False
ciReactionAllowed :: ChatItem c d -> Bool
ciReactionAllowed ChatItem {meta = CIMeta {itemDeleted = Just _}} = False
ciReactionAllowed ChatItem {content} = isJust $ ciMsgContent content
data ChatDirection (c :: ChatType) (d :: MsgDirection) where
CDDirectSnd :: Contact -> ChatDirection 'CTDirect 'MDSnd
CDDirectRcv :: Contact -> ChatDirection 'CTDirect 'MDRcv
CDGroupSnd :: GroupInfo -> Maybe GroupChatScopeInfo -> ChatDirection 'CTGroup 'MDSnd
CDGroupRcv :: GroupInfo -> Maybe GroupChatScopeInfo -> GroupMember -> ChatDirection 'CTGroup 'MDRcv
CDLocalSnd :: NoteFolder -> ChatDirection 'CTLocal 'MDSnd
CDLocalRcv :: NoteFolder -> ChatDirection 'CTLocal 'MDRcv
toCIDirection :: ChatDirection c d -> CIDirection c d
toCIDirection = \case
CDDirectSnd _ -> CIDirectSnd
CDDirectRcv _ -> CIDirectRcv
CDGroupSnd _ _ -> CIGroupSnd
CDGroupRcv _ _ m -> CIGroupRcv m
CDLocalSnd _ -> CILocalSnd
CDLocalRcv _ -> CILocalRcv
toChatInfo :: ChatDirection c d -> ChatInfo c
toChatInfo = \case
CDDirectSnd c -> DirectChat c
CDDirectRcv c -> DirectChat c
CDGroupSnd g s -> GroupChat g s
CDGroupRcv g s _ -> GroupChat g s
CDLocalSnd l -> LocalChat l
CDLocalRcv l -> LocalChat l
contactChatDeleted :: ChatDirection c d -> Bool
contactChatDeleted = \case
CDDirectSnd Contact {chatDeleted} -> chatDeleted
CDDirectRcv Contact {chatDeleted} -> chatDeleted
_ -> False
data NewChatItem d = NewChatItem
{ createdByMsgId :: Maybe MessageId,
itemSent :: SMsgDirection d,
itemTs :: ChatItemTs,
itemContent :: CIContent d,
itemText :: Text,
itemStatus :: CIStatus d,
itemSharedMsgId :: Maybe SharedMsgId,
itemQuotedMsg :: Maybe QuotedMsg,
createdAt :: UTCTime
}
deriving (Show)
-- | type to show one chat with messages
data Chat c = Chat
{ chatInfo :: ChatInfo c,
chatItems :: [CChatItem c],
chatStats :: ChatStats
}
deriving (Show)
data AChat = forall c. ChatTypeI c => AChat (SChatType c) (Chat c)
deriving instance Show AChat
data ChatStats = ChatStats
{ unreadCount :: Int, -- returned both in /_get chat initial API and in /_get chats API
unreadMentions :: Int, -- returned both in /_get chat initial API and in /_get chats API
reportsCount :: Int, -- returned both in /_get chat initial API and in /_get chats API
minUnreadItemId :: ChatItemId,
unreadChat :: Bool
}
deriving (Show)
emptyChatStats :: ChatStats
emptyChatStats = ChatStats 0 0 0 0 False
data NavigationInfo = NavigationInfo
{ afterUnread :: Int,
afterTotal :: Int
}
deriving (Show)
-- | type to show a mix of messages from multiple chats
data AChatItem = forall c d. (ChatTypeI c, MsgDirectionI d) => AChatItem (SChatType c) (SMsgDirection d) (ChatInfo c) (ChatItem c d)
deriving instance Show AChatItem
data JSONAnyChatItem c d = JSONAnyChatItem {chatInfo :: ChatInfo c, chatItem :: ChatItem c d}
aChatItems :: AChat -> [AChatItem]
aChatItems (AChat ct Chat {chatInfo, chatItems}) = map aChatItem chatItems
where
aChatItem (CChatItem md ci) = AChatItem ct md chatInfo ci
aChatItemId :: AChatItem -> Int64
aChatItemId (AChatItem _ _ _ ci) = chatItemId' ci
aChatItemTs :: AChatItem -> UTCTime
aChatItemTs (AChatItem _ _ _ ci) = chatItemTs' ci
aChatItemDir :: AChatItem -> MsgDirection
aChatItemDir (AChatItem _ sMsgDir _ _) = toMsgDirection sMsgDir
aChatItemRcvFromMember :: AChatItem -> Maybe GroupMember
aChatItemRcvFromMember (AChatItem _ _ _ ci) = chatItemRcvFromMember ci
aChatItemIsRcvNew :: AChatItem -> Bool
aChatItemIsRcvNew (AChatItem _ _ _ ci) = chatItemIsRcvNew ci
updateFileStatus :: forall c d. ChatItem c d -> CIFileStatus d -> ChatItem c d
updateFileStatus ci@ChatItem {file} status = case file of
Just f -> ci {file = Just (f :: CIFile d) {fileStatus = status}}
Nothing -> ci
-- This type is not saved to DB, so all JSON encodings are platform-specific
data CIMeta (c :: ChatType) (d :: MsgDirection) = CIMeta
{ itemId :: ChatItemId,
itemTs :: ChatItemTs,
itemText :: Text,
itemStatus :: CIStatus d,
sentViaProxy :: Maybe Bool,
itemSharedMsgId :: Maybe SharedMsgId,
itemForwarded :: Maybe CIForwardedFrom,
itemDeleted :: Maybe (CIDeleted c),
itemEdited :: Bool,
itemTimed :: Maybe CITimed,
itemLive :: Maybe Bool,
userMention :: Bool, -- True for messages that mention user or reply to user messages
deletable :: Bool,
editable :: Bool,
forwardedByMember :: Maybe GroupMemberId,
showGroupAsSender :: ShowGroupAsSender,
createdAt :: UTCTime,
updatedAt :: UTCTime
}
deriving (Show)
type ShowGroupAsSender = Bool
mkCIMeta :: forall c d. ChatTypeI c => ChatItemId -> CIContent d -> Text -> CIStatus d -> Maybe Bool -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe (CIDeleted c) -> Bool -> Maybe CITimed -> Maybe Bool -> Bool -> UTCTime -> ChatItemTs -> Maybe GroupMemberId -> Bool -> UTCTime -> UTCTime -> CIMeta c d
mkCIMeta itemId itemContent itemText itemStatus sentViaProxy itemSharedMsgId itemForwarded itemDeleted itemEdited itemTimed itemLive userMention currentTs itemTs forwardedByMember showGroupAsSender createdAt updatedAt =
let deletable = deletable' itemContent itemDeleted itemTs nominalDay currentTs
editable = deletable && isNothing itemForwarded
in CIMeta {itemId, itemTs, itemText, itemStatus, sentViaProxy, itemSharedMsgId, itemForwarded, itemDeleted, itemEdited, itemTimed, itemLive, userMention, deletable, editable, forwardedByMember, showGroupAsSender, createdAt, updatedAt}
deletable' :: forall c d. ChatTypeI c => CIContent d -> Maybe (CIDeleted c) -> UTCTime -> NominalDiffTime -> UTCTime -> Bool
deletable' itemContent itemDeleted itemTs allowedInterval currentTs =
case itemContent of
CISndMsgContent _ ->
case chatTypeI @c of
SCTLocal -> isNothing itemDeleted
_ -> diffUTCTime currentTs itemTs < allowedInterval && isNothing itemDeleted
_ -> False
dummyMeta :: ChatItemId -> UTCTime -> Text -> CIMeta c 'MDSnd
dummyMeta itemId ts itemText =
CIMeta
{ itemId,
itemTs = ts,
itemText,
itemStatus = CISSndNew,
sentViaProxy = Nothing,
itemSharedMsgId = Nothing,
itemForwarded = Nothing,
itemDeleted = Nothing,
itemEdited = False,
itemTimed = Nothing,
itemLive = Nothing,
userMention = False,
deletable = False,
editable = False,
forwardedByMember = Nothing,
showGroupAsSender = False,
createdAt = ts,
updatedAt = ts
}
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)
ttl' :: CITimed -> Int
ttl' CITimed {ttl} = ttl
contactTimedTTL :: Contact -> Maybe (Maybe Int)
contactTimedTTL Contact {mergedPreferences = ContactUserPreferences {timedMessages = ContactUserPreference {enabled, userPreference}}}
| forUser enabled && forContact enabled = Just ttl
| otherwise = Nothing
where
TimedMessagesPreference {ttl} = case userPreference of
CUPContact {preference} -> preference
CUPUser {preference} -> preference
groupTimedTTL :: GroupInfo -> Maybe (Maybe Int)
groupTimedTTL GroupInfo {fullGroupPreferences = FullGroupPreferences {timedMessages = TimedMessagesGroupPreference {enable, ttl}}}
| enable == FEOn = Just ttl
| otherwise = Nothing
rcvContactCITimed :: Contact -> Maybe Int -> Maybe CITimed
rcvContactCITimed = rcvCITimed_ . contactTimedTTL
rcvGroupCITimed :: GroupInfo -> Maybe Int -> Maybe CITimed
rcvGroupCITimed = rcvCITimed_ . groupTimedTTL
rcvCITimed_ :: Maybe (Maybe Int) -> Maybe Int -> Maybe CITimed
rcvCITimed_ chatTTL itemTTL = (`CITimed` Nothing) <$> (chatTTL >> itemTTL)
data CIQuote (c :: ChatType) = CIQuote
{ chatDir :: CIQDirection c,
itemId :: Maybe ChatItemId, -- Nothing in case MsgRef references the item the user did not receive yet
sharedMsgId :: Maybe SharedMsgId, -- Nothing for the messages from the old clients
sentAt :: UTCTime,
content :: MsgContent,
formattedText :: Maybe MarkdownList
}
deriving (Show)
quoteItemId :: CIQuote c -> Maybe ChatItemId
quoteItemId CIQuote {itemId} = itemId
data CIReaction (c :: ChatType) (d :: MsgDirection) = CIReaction
{ chatDir :: CIDirection c d,
chatItem :: CChatItem c,
sentAt :: UTCTime,
reaction :: MsgReaction
}
deriving (Show)
data AnyCIReaction = forall c d. ChatTypeI c => ACIR (SChatType c) (SMsgDirection d) (CIReaction c d)
data ACIReaction = forall c d. ChatTypeI c => ACIReaction (SChatType c) (SMsgDirection d) (ChatInfo c) (CIReaction c d)
deriving instance Show ACIReaction
data JSONCIReaction c d = JSONCIReaction {chatInfo :: ChatInfo c, chatReaction :: CIReaction c d}
data MemberReaction = MemberReaction
{ groupMember :: GroupMember,
reactionTs :: UTCTime
}
deriving (Show)
type family ChatTypeQuotable (a :: ChatType) :: Constraint where
ChatTypeQuotable 'CTDirect = ()
ChatTypeQuotable 'CTGroup = ()
ChatTypeQuotable a =
(Int ~ Bool, TypeError ('Type.Text "ChatType " ':<>: 'ShowType a ':<>: 'Type.Text " cannot be quoted"))
data CIQDirection (c :: ChatType) where
CIQDirectSnd :: CIQDirection 'CTDirect
CIQDirectRcv :: CIQDirection 'CTDirect
CIQGroupSnd :: CIQDirection 'CTGroup
CIQGroupRcv :: Maybe GroupMember -> CIQDirection 'CTGroup -- member can be Nothing in case MsgRef has memberId that the user is not notified about yet
deriving instance Show (CIQDirection c)
data ACIQDirection = forall c. (ChatTypeI c, ChatTypeQuotable c) => ACIQDirection (SChatType c) (CIQDirection c)
jsonCIQDirection :: CIQDirection c -> Maybe JSONCIDirection
jsonCIQDirection = \case
CIQDirectSnd -> Just JCIDirectSnd
CIQDirectRcv -> Just JCIDirectRcv
CIQGroupSnd -> Just JCIGroupSnd
CIQGroupRcv (Just m) -> Just $ JCIGroupRcv m
CIQGroupRcv Nothing -> Nothing
jsonACIQDirection :: Maybe JSONCIDirection -> Either String ACIQDirection
jsonACIQDirection = \case
Just JCIDirectSnd -> Right $ ACIQDirection SCTDirect CIQDirectSnd
Just JCIDirectRcv -> Right $ ACIQDirection SCTDirect CIQDirectRcv
Just JCIGroupSnd -> Right $ ACIQDirection SCTGroup CIQGroupSnd
Just (JCIGroupRcv m) -> Right $ ACIQDirection SCTGroup $ CIQGroupRcv (Just m)
Nothing -> Right $ ACIQDirection SCTGroup $ CIQGroupRcv Nothing
Just JCILocalSnd -> Left "unquotable"
Just JCILocalRcv -> Left "unquotable"
quoteMsgDirection :: CIQDirection c -> MsgDirection
quoteMsgDirection = \case
CIQDirectSnd -> MDSnd
CIQDirectRcv -> MDRcv
CIQGroupSnd -> MDSnd
CIQGroupRcv _ -> MDRcv
data CIFile (d :: MsgDirection) = CIFile
{ fileId :: Int64,
fileName :: String,
fileSize :: Integer,
fileSource :: Maybe CryptoFile, -- local file path with optional key and nonce
fileStatus :: CIFileStatus d,
fileProtocol :: FileProtocol
}
deriving (Show)
data FileProtocol = FPSMP | FPXFTP | FPLocal
deriving (Eq, Show, Ord)
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
instance TextEncoding FileProtocol where
textDecode = \case
"smp" -> Just FPSMP
"xftp" -> Just FPXFTP
"local" -> Just FPLocal
_ -> Nothing
textEncode = \case
FPSMP -> "smp"
FPXFTP -> "xftp"
FPLocal -> "local"
data CIFileStatus (d :: MsgDirection) where
CIFSSndStored :: CIFileStatus 'MDSnd
CIFSSndTransfer :: {sndProgress :: Int64, sndTotal :: Int64} -> CIFileStatus 'MDSnd
CIFSSndCancelled :: CIFileStatus 'MDSnd
CIFSSndComplete :: CIFileStatus 'MDSnd
CIFSSndError :: {sndFileError :: FileError} -> CIFileStatus 'MDSnd
CIFSSndWarning :: {sndFileError :: FileError} -> CIFileStatus 'MDSnd
CIFSRcvInvitation :: CIFileStatus 'MDRcv
CIFSRcvAccepted :: CIFileStatus 'MDRcv
CIFSRcvTransfer :: {rcvProgress :: Int64, rcvTotal :: Int64} -> CIFileStatus 'MDRcv
CIFSRcvAborted :: CIFileStatus 'MDRcv
CIFSRcvComplete :: CIFileStatus 'MDRcv
CIFSRcvCancelled :: CIFileStatus 'MDRcv
CIFSRcvError :: {rcvFileError :: FileError} -> CIFileStatus 'MDRcv
CIFSRcvWarning :: {rcvFileError :: FileError} -> CIFileStatus 'MDRcv
CIFSInvalid :: {text :: Text} -> CIFileStatus 'MDSnd
deriving instance Eq (CIFileStatus d)
deriving instance Show (CIFileStatus d)
ciFileEnded :: CIFileStatus d -> Bool
ciFileEnded = \case
CIFSSndStored -> False
CIFSSndTransfer {} -> False
CIFSSndCancelled -> True
CIFSSndComplete -> True
CIFSSndError {} -> True
CIFSSndWarning {} -> False
CIFSRcvInvitation -> False
CIFSRcvAccepted -> False
CIFSRcvTransfer {} -> False
CIFSRcvAborted -> True
CIFSRcvCancelled -> True
CIFSRcvComplete -> True
CIFSRcvError {} -> True
CIFSRcvWarning {} -> False
CIFSInvalid {} -> True
ciFileLoaded :: CIFileStatus d -> Bool
ciFileLoaded = \case
CIFSSndStored -> True
CIFSSndTransfer {} -> True
CIFSSndComplete -> True
CIFSSndCancelled -> True
CIFSSndError {} -> True
CIFSSndWarning {} -> True
CIFSRcvInvitation -> False
CIFSRcvAccepted -> False
CIFSRcvTransfer {} -> False
CIFSRcvAborted -> False
CIFSRcvCancelled -> False
CIFSRcvComplete -> True
CIFSRcvError {} -> False
CIFSRcvWarning {} -> False
CIFSInvalid {} -> False
data ForwardFileError = FFENotAccepted FileTransferId | FFEInProgress | FFEFailed | FFEMissing
deriving (Eq, Ord)
ciFileForwardError :: FileTransferId -> CIFileStatus d -> Maybe ForwardFileError
ciFileForwardError fId = \case
CIFSSndStored -> Nothing
CIFSSndTransfer {} -> Nothing
CIFSSndComplete -> Nothing
CIFSSndCancelled -> Nothing
CIFSSndError {} -> Nothing
CIFSSndWarning {} -> Nothing
CIFSRcvInvitation -> Just $ FFENotAccepted fId
CIFSRcvAccepted -> Just FFEInProgress
CIFSRcvTransfer {} -> Just FFEInProgress
CIFSRcvAborted -> Just $ FFENotAccepted fId
CIFSRcvCancelled -> Just FFEFailed
CIFSRcvComplete -> Nothing
CIFSRcvError {} -> Just FFEFailed
CIFSRcvWarning {} -> Just FFEFailed
CIFSInvalid {} -> Just FFEFailed
data ACIFileStatus = forall d. MsgDirectionI d => AFS (SMsgDirection d) (CIFileStatus d)
deriving instance Show ACIFileStatus
instance MsgDirectionI d => StrEncoding (CIFileStatus d) where
strEncode = \case
CIFSSndStored -> "snd_stored"
CIFSSndTransfer sent total -> strEncode (Str "snd_transfer", sent, total)
CIFSSndCancelled -> "snd_cancelled"
CIFSSndComplete -> "snd_complete"
CIFSSndError sndFileErr -> "snd_error " <> strEncode sndFileErr
CIFSSndWarning sndFileErr -> "snd_warning " <> strEncode sndFileErr
CIFSRcvInvitation -> "rcv_invitation"
CIFSRcvAccepted -> "rcv_accepted"
CIFSRcvTransfer rcvd total -> strEncode (Str "rcv_transfer", rcvd, total)
CIFSRcvAborted -> "rcv_aborted"
CIFSRcvComplete -> "rcv_complete"
CIFSRcvCancelled -> "rcv_cancelled"
CIFSRcvError rcvFileErr -> "rcv_error " <> strEncode rcvFileErr
CIFSRcvWarning rcvFileErr -> "rcv_warning " <> strEncode rcvFileErr
CIFSInvalid {} -> "invalid"
strP = (\(AFS _ st) -> checkDirection st) <$?> strP
instance StrEncoding ACIFileStatus where
strEncode (AFS _ s) = strEncode s
strP =
(statusP <* A.endOfInput) -- endOfInput to make it fail on partial correct parse
<|> (AFS SMDSnd . CIFSInvalid . safeDecodeUtf8 <$> A.takeByteString)
where
statusP =
A.takeTill (== ' ') >>= \case
"snd_stored" -> pure $ AFS SMDSnd CIFSSndStored
"snd_transfer" -> AFS SMDSnd <$> progress CIFSSndTransfer
"snd_cancelled" -> pure $ AFS SMDSnd CIFSSndCancelled
"snd_complete" -> pure $ AFS SMDSnd CIFSSndComplete
"snd_error" -> AFS SMDSnd . CIFSSndError <$> ((A.space *> strP) <|> pure (FileErrOther "")) -- alternative for backwards compatibility
"snd_warning" -> AFS SMDSnd . CIFSSndWarning <$> (A.space *> strP)
"rcv_invitation" -> pure $ AFS SMDRcv CIFSRcvInvitation
"rcv_accepted" -> pure $ AFS SMDRcv CIFSRcvAccepted
"rcv_transfer" -> AFS SMDRcv <$> progress CIFSRcvTransfer
"rcv_aborted" -> pure $ AFS SMDRcv CIFSRcvAborted
"rcv_complete" -> pure $ AFS SMDRcv CIFSRcvComplete
"rcv_cancelled" -> pure $ AFS SMDRcv CIFSRcvCancelled
"rcv_error" -> AFS SMDRcv . CIFSRcvError <$> ((A.space *> strP) <|> pure (FileErrOther "")) -- alternative for backwards compatibility
"rcv_warning" -> AFS SMDRcv . CIFSRcvWarning <$> (A.space *> strP)
_ -> fail "bad file status"
progress :: (Int64 -> Int64 -> a) -> A.Parser a
progress f = f <$> num <*> num <|> pure (f 0 1)
num = A.space *> A.decimal
data JSONCIFileStatus
= JCIFSSndStored
| JCIFSSndTransfer {sndProgress :: Int64, sndTotal :: Int64}
| JCIFSSndCancelled
| JCIFSSndComplete
| JCIFSSndError {sndFileError :: FileError}
| JCIFSSndWarning {sndFileError :: FileError}
| JCIFSRcvInvitation
| JCIFSRcvAccepted
| JCIFSRcvTransfer {rcvProgress :: Int64, rcvTotal :: Int64}
| JCIFSRcvAborted
| JCIFSRcvComplete
| JCIFSRcvCancelled
| JCIFSRcvError {rcvFileError :: FileError}
| JCIFSRcvWarning {rcvFileError :: FileError}
| JCIFSInvalid {text :: Text}
jsonCIFileStatus :: CIFileStatus d -> JSONCIFileStatus
jsonCIFileStatus = \case
CIFSSndStored -> JCIFSSndStored
CIFSSndTransfer sent total -> JCIFSSndTransfer sent total
CIFSSndCancelled -> JCIFSSndCancelled
CIFSSndComplete -> JCIFSSndComplete
CIFSSndError sndFileErr -> JCIFSSndError sndFileErr
CIFSSndWarning sndFileErr -> JCIFSSndWarning sndFileErr
CIFSRcvInvitation -> JCIFSRcvInvitation
CIFSRcvAccepted -> JCIFSRcvAccepted
CIFSRcvTransfer rcvd total -> JCIFSRcvTransfer rcvd total
CIFSRcvAborted -> JCIFSRcvAborted
CIFSRcvComplete -> JCIFSRcvComplete
CIFSRcvCancelled -> JCIFSRcvCancelled
CIFSRcvError rcvFileErr -> JCIFSRcvError rcvFileErr
CIFSRcvWarning rcvFileErr -> JCIFSRcvWarning rcvFileErr
CIFSInvalid text -> JCIFSInvalid text
aciFileStatusJSON :: JSONCIFileStatus -> ACIFileStatus
aciFileStatusJSON = \case
JCIFSSndStored -> AFS SMDSnd CIFSSndStored
JCIFSSndTransfer sent total -> AFS SMDSnd $ CIFSSndTransfer sent total
JCIFSSndCancelled -> AFS SMDSnd CIFSSndCancelled
JCIFSSndComplete -> AFS SMDSnd CIFSSndComplete
JCIFSSndError sndFileErr -> AFS SMDSnd (CIFSSndError sndFileErr)
JCIFSSndWarning sndFileErr -> AFS SMDSnd (CIFSSndWarning sndFileErr)
JCIFSRcvInvitation -> AFS SMDRcv CIFSRcvInvitation
JCIFSRcvAccepted -> AFS SMDRcv CIFSRcvAccepted
JCIFSRcvTransfer rcvd total -> AFS SMDRcv $ CIFSRcvTransfer rcvd total
JCIFSRcvAborted -> AFS SMDRcv CIFSRcvAborted
JCIFSRcvComplete -> AFS SMDRcv CIFSRcvComplete
JCIFSRcvCancelled -> AFS SMDRcv CIFSRcvCancelled
JCIFSRcvError rcvFileErr -> AFS SMDRcv (CIFSRcvError rcvFileErr)
JCIFSRcvWarning rcvFileErr -> AFS SMDRcv (CIFSRcvWarning rcvFileErr)
JCIFSInvalid text -> AFS SMDSnd $ CIFSInvalid text
data FileError
= FileErrAuth
| FileErrBlocked {server :: String, blockInfo :: BlockingInfo}
| FileErrNoFile
| FileErrRelay {srvError :: SrvError}
| FileErrOther {fileError :: Text}
deriving (Eq, Show)
instance StrEncoding FileError where
strEncode = \case
FileErrAuth -> "auth"
FileErrBlocked srv info -> "blocked " <> strEncode (srv, info)
FileErrNoFile -> "no_file"
FileErrRelay srvErr -> "relay " <> strEncode srvErr
FileErrOther e -> "other " <> encodeUtf8 e
strP =
A.takeWhile1 (/= ' ') >>= \case
"auth" -> pure FileErrAuth
"blocked" -> FileErrBlocked <$> _strP <*> _strP
"no_file" -> pure FileErrNoFile
"relay" -> FileErrRelay <$> _strP
"other" -> FileErrOther . safeDecodeUtf8 <$> (A.space *> A.takeByteString)
s -> FileErrOther . safeDecodeUtf8 . (s <>) <$> A.takeByteString
-- to conveniently read file data from db
data CIFileInfo = CIFileInfo
{ fileId :: Int64,
fileStatus :: Maybe ACIFileStatus,
filePath :: Maybe FilePath
}
deriving (Show)
mkCIFileInfo :: MsgDirectionI d => CIFile d -> CIFileInfo
mkCIFileInfo CIFile {fileId, fileStatus, fileSource} =
CIFileInfo
{ fileId,
fileStatus = Just $ AFS msgDirection fileStatus,
filePath = CF.filePath <$> fileSource
}
data CIStatus (d :: MsgDirection) where
CISSndNew :: CIStatus 'MDSnd
CISSndSent :: SndCIStatusProgress -> CIStatus 'MDSnd
CISSndRcvd :: MsgReceiptStatus -> SndCIStatusProgress -> CIStatus 'MDSnd
CISSndErrorAuth :: CIStatus 'MDSnd -- deprecated
CISSndError :: SndError -> CIStatus 'MDSnd
CISSndWarning :: SndError -> CIStatus 'MDSnd
CISRcvNew :: CIStatus 'MDRcv
CISRcvRead :: CIStatus 'MDRcv
CISInvalid :: Text -> CIStatus 'MDSnd
deriving instance Eq (CIStatus d)
deriving instance Show (CIStatus d)
data ACIStatus = forall d. MsgDirectionI d => ACIStatus (SMsgDirection d) (CIStatus d)
deriving instance Show ACIStatus
instance MsgDirectionI d => StrEncoding (CIStatus d) where
strEncode = \case
CISSndNew -> "snd_new"
CISSndSent sndProgress -> "snd_sent " <> strEncode sndProgress
CISSndRcvd msgRcptStatus sndProgress -> "snd_rcvd " <> strEncode msgRcptStatus <> " " <> strEncode sndProgress
CISSndErrorAuth -> "snd_error_auth"
CISSndError sndErr -> "snd_error " <> strEncode sndErr
CISSndWarning sndErr -> "snd_warning " <> strEncode sndErr
CISRcvNew -> "rcv_new"
CISRcvRead -> "rcv_read"
CISInvalid {} -> "invalid"
strP = (\(ACIStatus _ st) -> checkDirection st) <$?> strP
instance StrEncoding ACIStatus where
strEncode (ACIStatus _ s) = strEncode s
strP =
(statusP <* A.endOfInput) -- endOfInput to make it fail on partial correct parse, e.g. "snd_rcvd ok complete"
<|> (ACIStatus SMDSnd . CISInvalid . safeDecodeUtf8 <$> A.takeByteString)
where
statusP =
A.takeTill (== ' ') >>= \case
"snd_new" -> pure $ ACIStatus SMDSnd CISSndNew
"snd_sent" -> ACIStatus SMDSnd . CISSndSent <$> ((A.space *> strP) <|> pure SSPComplete)
"snd_rcvd" -> ACIStatus SMDSnd <$> (CISSndRcvd <$> (A.space *> strP) <*> ((A.space *> strP) <|> pure SSPComplete))
"snd_error_auth" -> pure $ ACIStatus SMDSnd CISSndErrorAuth
"snd_error" -> ACIStatus SMDSnd . CISSndError <$> (A.space *> strP)
"snd_warning" -> ACIStatus SMDSnd . CISSndWarning <$> (A.space *> strP)
"rcv_new" -> pure $ ACIStatus SMDRcv CISRcvNew
"rcv_read" -> pure $ ACIStatus SMDRcv CISRcvRead
_ -> fail "bad status"
-- see serverHostError in agent
data SndError
= SndErrAuth
| SndErrQuota
| SndErrExpired -- TIMEOUT/NETWORK errors
| SndErrRelay {srvError :: SrvError} -- BROKER errors (other than TIMEOUT/NETWORK)
| SndErrProxy {proxyServer :: String, srvError :: SrvError} -- SMP PROXY errors
| SndErrProxyRelay {proxyServer :: String, srvError :: SrvError} -- PROXY BROKER errors
| SndErrOther {sndError :: Text} -- other errors
deriving (Eq, Show)
data SrvError
= SrvErrHost
| SrvErrVersion
| SrvErrOther {srvError :: Text}
deriving (Eq, Show)
instance StrEncoding SndError where
strEncode = \case
SndErrAuth -> "auth"
SndErrQuota -> "quota"
SndErrExpired -> "expired"
SndErrRelay srvErr -> "relay " <> strEncode srvErr
SndErrProxy proxy srvErr -> "proxy " <> encodeUtf8 (T.pack proxy) <> " " <> strEncode srvErr
SndErrProxyRelay proxy srvErr -> "proxy_relay " <> encodeUtf8 (T.pack proxy) <> " " <> strEncode srvErr
SndErrOther e -> "other " <> encodeUtf8 e
strP =
A.takeWhile1 (/= ' ') >>= \case
"auth" -> pure SndErrAuth
"quota" -> pure SndErrQuota
"expired" -> pure SndErrExpired
"relay" -> SndErrRelay <$> (A.space *> strP)
"proxy" -> SndErrProxy . T.unpack . safeDecodeUtf8 <$> (A.space *> A.takeWhile1 (/= ' ') <* A.space) <*> strP
"proxy_relay" -> SndErrProxyRelay . T.unpack . safeDecodeUtf8 <$> (A.space *> A.takeWhile1 (/= ' ') <* A.space) <*> strP
"other" -> SndErrOther . safeDecodeUtf8 <$> (A.space *> A.takeByteString)
s -> SndErrOther . safeDecodeUtf8 . (s <>) <$> A.takeByteString -- for backward compatibility with `CISSndError String`
instance StrEncoding SrvError where
strEncode = \case
SrvErrHost -> "host"
SrvErrVersion -> "version"
SrvErrOther e -> "other " <> encodeUtf8 e
strP =
A.takeWhile1 (/= ' ') >>= \case
"host" -> pure SrvErrHost
"version" -> pure SrvErrVersion
"other" -> SrvErrOther . safeDecodeUtf8 <$> (A.space *> A.takeByteString)
_ -> fail "bad SrvError"
data JSONCIStatus
= JCISSndNew
| JCISSndSent {sndProgress :: SndCIStatusProgress}
| JCISSndRcvd {msgRcptStatus :: MsgReceiptStatus, sndProgress :: SndCIStatusProgress}
| JCISSndErrorAuth -- deprecated
| JCISSndError {agentError :: SndError}
| JCISSndWarning {agentError :: SndError}
| JCISRcvNew
| JCISRcvRead
| JCISInvalid {text :: Text}
deriving (Show)
jsonCIStatus :: CIStatus d -> JSONCIStatus
jsonCIStatus = \case
CISSndNew -> JCISSndNew
CISSndSent sndProgress -> JCISSndSent sndProgress
CISSndRcvd msgRcptStatus sndProgress -> JCISSndRcvd msgRcptStatus sndProgress
CISSndErrorAuth -> JCISSndErrorAuth
CISSndError sndErr -> JCISSndError sndErr
CISSndWarning sndErr -> JCISSndWarning sndErr
CISRcvNew -> JCISRcvNew
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 sndErr -> ACIStatus SMDSnd $ CISSndError sndErr
JCISSndWarning sndErr -> ACIStatus SMDSnd $ CISSndWarning sndErr
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
SMDRcv -> CISRcvNew
ciCreateStatus :: forall d. MsgDirectionI d => CIContent d -> CIStatus d
ciCreateStatus content = case msgDirection @d of
SMDSnd -> ciStatusNew
SMDRcv
| isCIReport content -> CISRcvRead
| ciRequiresAttention content -> ciStatusNew
| otherwise -> CISRcvRead
membersGroupItemStatus :: [(GroupSndStatus, Int)] -> CIStatus 'MDSnd
membersGroupItemStatus memStatusCounts
| rcvdOk == total = CISSndRcvd MROk SSPComplete
| rcvdOk + rcvdBad == total = CISSndRcvd MRBadMsgHash SSPComplete
| rcvdBad > 0 = CISSndRcvd MRBadMsgHash SSPPartial
| rcvdOk > 0 = CISSndRcvd MROk SSPPartial
| sent == total = CISSndSent SSPComplete
| sent > 0 = CISSndSent SSPPartial
| otherwise = CISSndNew
where
total = sum $ map snd memStatusCounts
rcvdOk = fromMaybe 0 $ lookup (GSSRcvd MROk) memStatusCounts
rcvdBad = fromMaybe 0 $ lookup (GSSRcvd MRBadMsgHash) memStatusCounts
sent = fromMaybe 0 $ lookup GSSSent memStatusCounts
data SndCIStatusProgress
= SSPPartial
| SSPComplete
deriving (Eq, Show)
instance StrEncoding SndCIStatusProgress where
strEncode = \case
SSPPartial -> "partial"
SSPComplete -> "complete"
strP =
A.takeWhile1 (/= ' ') >>= \case
"partial" -> pure SSPPartial
"complete" -> pure SSPComplete
_ -> fail "bad SndCIStatusProgress"
data GroupSndStatus
= GSSNew
| GSSForwarded
| GSSInactive
| GSSSent
| GSSRcvd {msgRcptStatus :: MsgReceiptStatus}
| GSSError {agentError :: SndError}
| GSSWarning {agentError :: SndError}
| GSSInvalid {text :: Text}
deriving instance Eq GroupSndStatus
deriving instance Show GroupSndStatus
-- Preserve CIStatus encoding for backwards compatibility
instance StrEncoding GroupSndStatus where
strEncode = \case
GSSNew -> "snd_new"
GSSForwarded -> "snd_forwarded"
GSSInactive -> "snd_inactive"
GSSSent -> "snd_sent complete"
GSSRcvd msgRcptStatus -> "snd_rcvd " <> strEncode msgRcptStatus <> " complete"
GSSError sndErr -> "snd_error " <> strEncode sndErr
GSSWarning sndErr -> "snd_warning " <> strEncode sndErr
GSSInvalid {} -> "invalid"
strP =
(statusP <* A.endOfInput) -- see ACIStatus decoding
<|> (GSSInvalid . safeDecodeUtf8 <$> A.takeByteString)
where
statusP =
A.takeTill (== ' ') >>= \case
"snd_new" -> pure GSSNew
"snd_forwarded" -> pure GSSForwarded
"snd_inactive" -> pure GSSInactive
"snd_sent" -> GSSSent <$ " complete"
"snd_rcvd" -> GSSRcvd <$> (_strP <* " complete")
"snd_error_auth" -> pure $ GSSError SndErrAuth
"snd_error" -> GSSError <$> (A.space *> strP)
"snd_warning" -> GSSWarning <$> (A.space *> strP)
_ -> fail "bad status"
type ChatItemId = Int64
type ChatItemTs = UTCTime
data SndMessage = SndMessage
{ msgId :: MessageId,
sharedMsgId :: SharedMsgId,
msgBody :: MsgBody
}
deriving (Show)
data NewRcvMessage e = NewRcvMessage
{ chatMsgEvent :: ChatMsgEvent e,
msgBody :: MsgBody
}
deriving (Show)
data RcvMessage = RcvMessage
{ msgId :: MessageId,
chatMsgEvent :: AChatMsgEvent,
sharedMsgId_ :: Maybe SharedMsgId,
msgBody :: MsgBody,
authorMember :: Maybe GroupMemberId,
forwardedByMember :: Maybe GroupMemberId
}
type MessageId = Int64
data ConnOrGroupId = ConnectionId Int64 | GroupId Int64
data SndMsgDelivery = SndMsgDelivery
{ connId :: Int64,
agentMsgId :: AgentMsgId
}
deriving (Show)
data RcvMsgDelivery = RcvMsgDelivery
{ connId :: Int64,
agentMsgId :: AgentMsgId,
agentMsgMeta :: MsgMeta
}
deriving (Show)
data RcvMsgInfo = RcvMsgInfo
{ msgId :: Int64,
msgDeliveryId :: Int64,
msgDeliveryStatus :: Text,
agentMsgId :: AgentMsgId,
agentMsgMeta :: Text
}
deriving (Show)
data MsgMetaJSON = MsgMetaJSON
{ integrity :: Text,
rcvId :: Int64,
rcvTs :: UTCTime,
serverId :: Text,
serverTs :: UTCTime,
sndId :: Int64
}
deriving (Eq, Show)
msgMetaToJson :: MsgMeta -> MsgMetaJSON
msgMetaToJson MsgMeta {integrity, recipient = (rcvId, rcvTs), broker = (serverId, serverTs), sndMsgId = sndId} =
MsgMetaJSON
{ integrity = (decodeLatin1 . strEncode) integrity,
rcvId,
rcvTs,
serverId = (decodeLatin1 . B64.encode) serverId,
serverTs,
sndId
}
data MsgDeliveryStatus (d :: MsgDirection) where
MDSRcvAgent :: MsgDeliveryStatus 'MDRcv
MDSRcvAcknowledged :: MsgDeliveryStatus 'MDRcv -- not used
MDSSndPending :: MsgDeliveryStatus 'MDSnd
MDSSndAgent :: MsgDeliveryStatus 'MDSnd
MDSSndSent :: MsgDeliveryStatus 'MDSnd
MDSSndRcvd :: MsgReceiptStatus -> MsgDeliveryStatus 'MDSnd
MDSSndRead :: MsgDeliveryStatus 'MDSnd
data AMsgDeliveryStatus = forall d. AMDS (SMsgDirection d) (MsgDeliveryStatus d)
instance (Typeable d, MsgDirectionI d) => FromField (MsgDeliveryStatus d) where
fromField = fromTextField_ msgDeliveryStatusT'
instance ToField (MsgDeliveryStatus d) where toField = toField . serializeMsgDeliveryStatus
serializeMsgDeliveryStatus :: MsgDeliveryStatus d -> Text
serializeMsgDeliveryStatus = \case
MDSRcvAgent -> "rcv_agent"
MDSRcvAcknowledged -> "rcv_acknowledged"
MDSSndPending -> "snd_pending"
MDSSndAgent -> "snd_agent"
MDSSndSent -> "snd_sent"
MDSSndRcvd status -> "snd_rcvd " <> safeDecodeUtf8 (strEncode status)
MDSSndRead -> "snd_read"
msgDeliveryStatusT :: Text -> Maybe AMsgDeliveryStatus
msgDeliveryStatusT = eitherToMaybe . parseAll statusP . encodeUtf8
where
statusP =
A.takeTill (== ' ') >>= \case
"rcv_agent" -> pure $ AMDS SMDRcv MDSRcvAgent
"rcv_acknowledged" -> pure $ AMDS SMDRcv MDSRcvAcknowledged
"snd_pending" -> pure $ AMDS SMDSnd MDSSndPending
"snd_agent" -> pure $ AMDS SMDSnd MDSSndAgent
"snd_sent" -> pure $ AMDS SMDSnd MDSSndSent
"snd_rcvd" -> AMDS SMDSnd . MDSSndRcvd <$> (A.space *> strP)
"snd_read" -> pure $ AMDS SMDSnd MDSSndRead
_ -> fail "bad AMsgDeliveryStatus"
msgDeliveryStatusT' :: forall d. MsgDirectionI d => Text -> Maybe (MsgDeliveryStatus d)
msgDeliveryStatusT' s =
msgDeliveryStatusT s >>= \(AMDS d st) ->
case testEquality d (msgDirection @d) of
Just Refl -> Just st
_ -> Nothing
data CIDeleted (c :: ChatType) where
CIDeleted :: Maybe UTCTime -> CIDeleted c
CIBlocked :: Maybe UTCTime -> CIDeleted 'CTGroup
CIBlockedByAdmin :: Maybe UTCTime -> CIDeleted 'CTGroup
CIModerated :: Maybe UTCTime -> GroupMember -> CIDeleted 'CTGroup
deriving instance Show (CIDeleted c)
data ACIDeleted = forall c. ChatTypeI c => ACIDeleted (SChatType c) (CIDeleted c)
data JSONCIDeleted
= JCIDDeleted {deletedTs :: Maybe UTCTime, chatType :: ChatType}
| JCIDBlocked {deletedTs :: Maybe UTCTime}
| JCIDBlockedByAdmin {deletedTs :: Maybe UTCTime}
| JCIDModerated {deletedTs :: Maybe UTCTime, byGroupMember :: GroupMember}
deriving (Show)
jsonCIDeleted :: forall d. ChatTypeI d => CIDeleted d -> JSONCIDeleted
jsonCIDeleted = \case
CIDeleted ts -> JCIDDeleted ts (toChatType $ chatTypeI @d)
CIBlocked ts -> JCIDBlocked ts
CIBlockedByAdmin ts -> JCIDBlockedByAdmin ts
CIModerated ts m -> JCIDModerated ts m
jsonACIDeleted :: JSONCIDeleted -> ACIDeleted
jsonACIDeleted = \case
JCIDDeleted ts cType -> case aChatType cType of ACT c -> ACIDeleted c $ CIDeleted ts
JCIDBlocked ts -> ACIDeleted SCTGroup $ CIBlocked ts
JCIDBlockedByAdmin ts -> ACIDeleted SCTGroup $ CIBlockedByAdmin ts
JCIDModerated ts m -> ACIDeleted SCTGroup (CIModerated ts m)
itemDeletedTs :: CIDeleted d -> Maybe UTCTime
itemDeletedTs = \case
CIDeleted ts -> ts
CIBlocked ts -> ts
CIBlockedByAdmin ts -> ts
CIModerated ts _ -> ts
data CIForwardedFrom
= CIFFUnknown
| CIFFContact {chatName :: Text, msgDir :: MsgDirection, contactId :: Maybe ContactId, chatItemId :: Maybe ChatItemId}
| CIFFGroup {chatName :: Text, msgDir :: MsgDirection, groupId :: Maybe GroupId, chatItemId :: Maybe ChatItemId}
deriving (Show)
cmForwardedFrom :: AChatMsgEvent -> Maybe CIForwardedFrom
cmForwardedFrom = \case
ACME _ (XMsgNew (MCForward _)) -> Just CIFFUnknown
_ -> Nothing
data CIForwardedFromTag
= CIFFUnknown_
| CIFFContact_
| CIFFGroup_
instance FromField CIForwardedFromTag where fromField = fromTextField_ textDecode
instance ToField CIForwardedFromTag where toField = toField . textEncode
instance TextEncoding CIForwardedFromTag where
textDecode = \case
"unknown" -> Just CIFFUnknown_
"contact" -> Just CIFFContact_
"group" -> Just CIFFGroup_
_ -> Nothing
textEncode = \case
CIFFUnknown_ -> "unknown"
CIFFContact_ -> "contact"
CIFFGroup_ -> "group"
data ChatItemInfo = ChatItemInfo
{ itemVersions :: [ChatItemVersion],
memberDeliveryStatuses :: Maybe (NonEmpty MemberDeliveryStatus),
forwardedFromChatItem :: Maybe AChatItem
}
deriving (Show)
data ChatItemVersion = ChatItemVersion
{ chatItemVersionId :: Int64,
msgContent :: MsgContent,
formattedText :: Maybe MarkdownList,
itemVersionTs :: UTCTime,
createdAt :: UTCTime
}
deriving (Eq, Show)
mkItemVersion :: ChatItem c d -> Maybe ChatItemVersion
mkItemVersion ChatItem {content, formattedText, meta} = version <$> ciMsgContent content
where
CIMeta {itemId, itemTs, createdAt} = meta
version mc =
ChatItemVersion
{ chatItemVersionId = itemId,
msgContent = mc,
formattedText,
itemVersionTs = itemTs,
createdAt = createdAt
}
data MemberDeliveryStatus = MemberDeliveryStatus
{ groupMemberId :: GroupMemberId,
memberDeliveryStatus :: GroupSndStatus,
sentViaProxy :: Maybe Bool
}
deriving (Eq, Show)
data CIModeration = CIModeration
{ moderationId :: Int64,
moderatorMember :: GroupMember,
createdByMsgId :: MessageId,
moderatedAt :: UTCTime
}
deriving (Show)
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
$(JQ.deriveJSON defaultJSON ''ChatName)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "JCID") ''JSONCIDeleted)
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
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CIFF") ''CIForwardedFrom)
$(JQ.deriveJSON defaultJSON ''CITimed)
$(JQ.deriveJSON (enumJSON $ dropPrefix "SSP") ''SndCIStatusProgress)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "SrvErr") ''SrvError)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "SndErr") ''SndError)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "JCIS") ''JSONCIStatus)
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
instance MsgDirectionI d => ToField (CIStatus d) where toField = toField . decodeLatin1 . strEncode
instance (Typeable d, MsgDirectionI d) => FromField (CIStatus d) where fromField = fromTextField_ $ eitherToMaybe . strDecode . encodeUtf8
instance FromField ACIStatus where fromField = fromTextField_ $ eitherToMaybe . strDecode . encodeUtf8
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "GSS") ''GroupSndStatus)
instance ToField GroupSndStatus where toField = toField . decodeLatin1 . strEncode
instance FromField GroupSndStatus where fromField = fromTextField_ $ eitherToMaybe . strDecode . encodeUtf8
$(JQ.deriveJSON defaultJSON ''MemberDeliveryStatus)
$(JQ.deriveJSON defaultJSON ''ChatItemVersion)
instance (ChatTypeI c, MsgDirectionI d) => FromJSON (CIMeta c d) where
parseJSON = $(JQ.mkParseJSON defaultJSON ''CIMeta)
instance ChatTypeI c => ToJSON (CIMeta c d) where
toJSON = $(JQ.mkToJSON defaultJSON ''CIMeta)
toEncoding = $(JQ.mkToEncoding defaultJSON ''CIMeta)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "FileErr") ''FileError)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "JCIFS") ''JSONCIFileStatus)
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
instance MsgDirectionI d => ToField (CIFileStatus d) where toField = toField . decodeLatin1 . strEncode
instance FromField ACIFileStatus where fromField = fromTextField_ $ eitherToMaybe . strDecode . encodeUtf8
instance MsgDirectionI d => FromJSON (CIFile d) where
parseJSON = $(JQ.mkParseJSON defaultJSON ''CIFile)
instance MsgDirectionI d => ToJSON (CIFile d) where
toJSON = $(JQ.mkToJSON defaultJSON ''CIFile)
toEncoding = $(JQ.mkToEncoding defaultJSON ''CIFile)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "GCS") ''GroupChatScope)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "JCI") ''JSONCIDirection)
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
instance ChatTypeI c => FromJSON (CCIDirection c) where
parseJSON v = (\(ACID _ d x) -> checkChatType (CCID d x)) <$?> J.parseJSON v
instance FromJSON ACIDirection where
parseJSON v = jsonACIDirection <$> J.parseJSON v
instance ChatTypeI c => FromJSON (CIQDirection c) where
parseJSON v = (jsonACIQDirection >=> \(ACIQDirection _ x) -> checkChatType x) <$?> J.parseJSON v
instance ToJSON (CIQDirection c) where
toJSON = J.toJSON . jsonCIQDirection
toEncoding = J.toEncoding . jsonCIQDirection
instance ChatTypeI c => FromJSON (CIQuote c) where
parseJSON = $(JQ.mkParseJSON defaultJSON ''CIQuote)
$(JQ.deriveToJSON defaultJSON ''CIQuote)
$(JQ.deriveJSON defaultJSON ''CIReactionCount)
$(JQ.deriveJSON defaultJSON ''CIMentionMember)
$(JQ.deriveJSON defaultJSON ''CIMention)
instance (ChatTypeI c, MsgDirectionI d) => FromJSON (ChatItem c d) where
parseJSON = $(JQ.mkParseJSON defaultJSON ''ChatItem)
instance (ChatTypeI c, MsgDirectionI d) => ToJSON (ChatItem c d) where
toJSON = $(JQ.mkToJSON defaultJSON ''ChatItem)
toEncoding = $(JQ.mkToEncoding defaultJSON ''ChatItem)
instance (ChatTypeI c, MsgDirectionI d) => ToJSON (JSONAnyChatItem c d) where
toJSON = $(JQ.mkToJSON defaultJSON ''JSONAnyChatItem)
toEncoding = $(JQ.mkToEncoding defaultJSON ''JSONAnyChatItem)
-- if JSON encoding changes, update AChatItem type definition in bots/src/API/Docs/Types.hs
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
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
$(JQ.deriveJSON defaultJSON ''ChatItemInfo)
$(JQ.deriveJSON defaultJSON ''ChatStats)
$(JQ.deriveJSON defaultJSON ''NavigationInfo)
instance ChatTypeI c => ToJSON (Chat c) where
toJSON = $(JQ.mkToJSON defaultJSON ''Chat)
toEncoding = $(JQ.mkToEncoding defaultJSON ''Chat)
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
instance (ChatTypeI c, MsgDirectionI d) => FromJSON (CIReaction c d) where
parseJSON = $(JQ.mkParseJSON defaultJSON ''CIReaction)
instance ChatTypeI c => ToJSON (CIReaction c d) where
toJSON = $(JQ.mkToJSON defaultJSON ''CIReaction)
toEncoding = $(JQ.mkToEncoding defaultJSON ''CIReaction)
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
instance ChatTypeI c => ToJSON (JSONCIReaction c d) where
toJSON = $(JQ.mkToJSON defaultJSON ''JSONCIReaction)
toEncoding = $(JQ.mkToEncoding defaultJSON ''JSONCIReaction)
-- if JSON encoding changes, update ACIReaction type definition in bots/src/API/Docs/Types.hs
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 _ _ cInfo reaction) = J.toJSON $ JSONCIReaction cInfo reaction
toEncoding (ACIReaction _ _ cInfo reaction) = J.toEncoding $ JSONCIReaction cInfo reaction
$(JQ.deriveJSON defaultJSON ''MemberReaction)
$(JQ.deriveJSON defaultJSON ''MsgMetaJSON)
msgMetaJson :: MsgMeta -> Text
msgMetaJson = decodeLatin1 . LB.toStrict . J.encode . msgMetaToJson
$(JQ.deriveJSON defaultJSON ''RcvMsgInfo)