2024-12-20 16:54:24 +04:00
|
|
|
{-# LANGUAGE BangPatterns #-}
|
|
|
|
{-# LANGUAGE DataKinds #-}
|
|
|
|
{-# LANGUAGE DuplicateRecordFields #-}
|
|
|
|
{-# LANGUAGE FlexibleContexts #-}
|
|
|
|
{-# LANGUAGE GADTs #-}
|
|
|
|
{-# LANGUAGE KindSignatures #-}
|
|
|
|
{-# LANGUAGE LambdaCase #-}
|
|
|
|
{-# LANGUAGE MultiWayIf #-}
|
|
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
|
|
|
{-# LANGUAGE OverloadedLists #-}
|
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
{-# LANGUAGE PatternSynonyms #-}
|
|
|
|
{-# LANGUAGE RankNTypes #-}
|
|
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
|
|
{-# LANGUAGE TupleSections #-}
|
|
|
|
{-# LANGUAGE TypeApplications #-}
|
|
|
|
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
|
|
|
|
|
|
|
module Simplex.Chat.Library.Internal where
|
|
|
|
|
|
|
|
import Control.Applicative ((<|>))
|
|
|
|
import Control.Concurrent.STM (retry)
|
|
|
|
import Control.Logger.Simple
|
|
|
|
import Control.Monad
|
|
|
|
import Control.Monad.Except
|
|
|
|
import Control.Monad.IO.Unlift
|
|
|
|
import Control.Monad.Reader
|
|
|
|
import Crypto.Random (ChaChaDRG)
|
|
|
|
import Data.Bifunctor (first)
|
|
|
|
import Data.ByteString.Char8 (ByteString)
|
|
|
|
import qualified Data.ByteString.Char8 as B
|
2025-01-30 10:06:26 +00:00
|
|
|
import Data.Char (isDigit)
|
2025-01-29 13:04:48 +00:00
|
|
|
import Data.Containers.ListUtils (nubOrd)
|
2024-12-20 16:54:24 +04:00
|
|
|
import Data.Either (partitionEithers, rights)
|
|
|
|
import Data.Fixed (div')
|
|
|
|
import Data.Foldable (foldr')
|
|
|
|
import Data.Functor (($>))
|
|
|
|
import Data.Functor.Identity
|
|
|
|
import Data.Int (Int64)
|
|
|
|
import Data.List (find, mapAccumL, partition)
|
|
|
|
import Data.List.NonEmpty (NonEmpty (..))
|
|
|
|
import qualified Data.List.NonEmpty as L
|
|
|
|
import Data.Map.Strict (Map)
|
|
|
|
import qualified Data.Map.Strict as M
|
|
|
|
import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, mapMaybe)
|
|
|
|
import qualified Data.Set as S
|
|
|
|
import Data.Text (Text)
|
|
|
|
import qualified Data.Text as T
|
|
|
|
import Data.Text.Encoding (encodeUtf8)
|
|
|
|
import Data.Time (addUTCTime)
|
|
|
|
import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime, nominalDiffTimeToSeconds)
|
|
|
|
import Simplex.Chat.Call
|
|
|
|
import Simplex.Chat.Controller
|
|
|
|
import Simplex.Chat.Files
|
|
|
|
import Simplex.Chat.Markdown
|
|
|
|
import Simplex.Chat.Messages
|
|
|
|
import Simplex.Chat.Messages.Batch (MsgBatch (..), batchMessages)
|
|
|
|
import Simplex.Chat.Messages.CIContent
|
|
|
|
import Simplex.Chat.Messages.CIContent.Events
|
|
|
|
import Simplex.Chat.Operators
|
|
|
|
import Simplex.Chat.ProfileGenerator (generateRandomProfile)
|
|
|
|
import Simplex.Chat.Protocol
|
|
|
|
import Simplex.Chat.Store
|
2025-01-31 18:47:59 +04:00
|
|
|
import Simplex.Chat.Store.Connections
|
2024-12-20 16:54:24 +04:00
|
|
|
import Simplex.Chat.Store.Direct
|
|
|
|
import Simplex.Chat.Store.Files
|
|
|
|
import Simplex.Chat.Store.Groups
|
|
|
|
import Simplex.Chat.Store.Messages
|
|
|
|
import Simplex.Chat.Store.Profiles
|
|
|
|
import Simplex.Chat.Store.Shared
|
|
|
|
import Simplex.Chat.Types
|
|
|
|
import Simplex.Chat.Types.Preferences
|
|
|
|
import Simplex.Chat.Types.Shared
|
|
|
|
import Simplex.Chat.Util (encryptFile, shuffle)
|
|
|
|
import Simplex.FileTransfer.Description (FileDescriptionURI (..), ValidFileDescription)
|
|
|
|
import qualified Simplex.FileTransfer.Description as FD
|
|
|
|
import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI)
|
|
|
|
import Simplex.FileTransfer.Types (RcvFileId, SndFileId)
|
|
|
|
import Simplex.Messaging.Agent as Agent
|
|
|
|
import Simplex.Messaging.Agent.Client (getFastNetworkConfig, ipAddressProtected, withLockMap)
|
|
|
|
import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..))
|
|
|
|
import Simplex.Messaging.Agent.Lock (withLock)
|
|
|
|
import Simplex.Messaging.Agent.Protocol
|
|
|
|
import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..))
|
2025-01-10 15:27:29 +04:00
|
|
|
import qualified Simplex.Messaging.Agent.Store.DB as DB
|
2024-12-20 16:54:24 +04:00
|
|
|
import Simplex.Messaging.Client (NetworkConfig (..))
|
|
|
|
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
|
|
|
|
import qualified Simplex.Messaging.Crypto.File as CF
|
|
|
|
import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), pattern IKPQOff, pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn)
|
|
|
|
import qualified Simplex.Messaging.Crypto.Ratchet as CR
|
|
|
|
import Simplex.Messaging.Encoding
|
|
|
|
import Simplex.Messaging.Encoding.String
|
|
|
|
import Simplex.Messaging.Protocol (MsgBody, MsgFlags (..), ProtoServerWithAuth (..), ProtocolServer, ProtocolTypeI (..), SProtocolType (..), SubscriptionMode (..), UserProtocol, XFTPServer)
|
|
|
|
import qualified Simplex.Messaging.Protocol as SMP
|
|
|
|
import qualified Simplex.Messaging.TMap as TM
|
|
|
|
import Simplex.Messaging.Util
|
|
|
|
import Simplex.Messaging.Version
|
|
|
|
import System.FilePath (takeFileName, (</>))
|
|
|
|
import System.IO (Handle, IOMode (..), SeekMode (..), hFlush)
|
|
|
|
import UnliftIO.Concurrent (forkFinally, mkWeakThreadId)
|
|
|
|
import UnliftIO.Directory
|
|
|
|
import UnliftIO.IO (hClose, hSeek, hTell, openFile)
|
|
|
|
import UnliftIO.STM
|
|
|
|
|
|
|
|
maxMsgReactions :: Int
|
|
|
|
maxMsgReactions = 3
|
|
|
|
|
2025-01-29 13:04:48 +00:00
|
|
|
maxRcvMentions :: Int
|
|
|
|
maxRcvMentions = 5
|
|
|
|
|
|
|
|
maxSndMentions :: Int
|
|
|
|
maxSndMentions = 3
|
|
|
|
|
2024-12-20 16:54:24 +04:00
|
|
|
withChatLock :: String -> CM a -> CM a
|
|
|
|
withChatLock name action = asks chatLock >>= \l -> withLock l name action
|
|
|
|
|
|
|
|
withEntityLock :: String -> ChatLockEntity -> CM a -> CM a
|
|
|
|
withEntityLock name entity action = do
|
|
|
|
chatLock <- asks chatLock
|
|
|
|
ls <- asks entityLocks
|
|
|
|
atomically $ unlessM (isEmptyTMVar chatLock) retry
|
|
|
|
withLockMap ls entity name action
|
|
|
|
|
|
|
|
withInvitationLock :: String -> ByteString -> CM a -> CM a
|
|
|
|
withInvitationLock name = withEntityLock name . CLInvitation
|
|
|
|
{-# INLINE withInvitationLock #-}
|
|
|
|
|
|
|
|
withConnectionLock :: String -> Int64 -> CM a -> CM a
|
|
|
|
withConnectionLock name = withEntityLock name . CLConnection
|
|
|
|
{-# INLINE withConnectionLock #-}
|
|
|
|
|
|
|
|
withContactLock :: String -> ContactId -> CM a -> CM a
|
|
|
|
withContactLock name = withEntityLock name . CLContact
|
|
|
|
{-# INLINE withContactLock #-}
|
|
|
|
|
|
|
|
withGroupLock :: String -> GroupId -> CM a -> CM a
|
|
|
|
withGroupLock name = withEntityLock name . CLGroup
|
|
|
|
{-# INLINE withGroupLock #-}
|
|
|
|
|
|
|
|
withUserContactLock :: String -> Int64 -> CM a -> CM a
|
|
|
|
withUserContactLock name = withEntityLock name . CLUserContact
|
|
|
|
{-# INLINE withUserContactLock #-}
|
|
|
|
|
|
|
|
withFileLock :: String -> Int64 -> CM a -> CM a
|
|
|
|
withFileLock name = withEntityLock name . CLFile
|
|
|
|
{-# INLINE withFileLock #-}
|
|
|
|
|
|
|
|
useServerCfgs :: forall p. UserProtocol p => SProtocolType p -> RandomAgentServers -> [(Text, ServerOperator)] -> [UserServer p] -> NonEmpty (ServerCfg p)
|
|
|
|
useServerCfgs p RandomAgentServers {smpServers, xftpServers} opDomains =
|
|
|
|
fromMaybe (rndAgentServers p) . L.nonEmpty . agentServerCfgs p opDomains
|
|
|
|
where
|
|
|
|
rndAgentServers :: SProtocolType p -> NonEmpty (ServerCfg p)
|
|
|
|
rndAgentServers = \case
|
|
|
|
SPSMP -> smpServers
|
|
|
|
SPXFTP -> xftpServers
|
|
|
|
|
|
|
|
contactCITimed :: Contact -> CM (Maybe CITimed)
|
|
|
|
contactCITimed ct = sndContactCITimed False ct Nothing
|
|
|
|
|
|
|
|
sndContactCITimed :: Bool -> Contact -> Maybe Int -> CM (Maybe CITimed)
|
|
|
|
sndContactCITimed live = sndCITimed_ live . contactTimedTTL
|
|
|
|
|
|
|
|
sndGroupCITimed :: Bool -> GroupInfo -> Maybe Int -> CM (Maybe CITimed)
|
|
|
|
sndGroupCITimed live = sndCITimed_ live . groupTimedTTL
|
|
|
|
|
|
|
|
sndCITimed_ :: Bool -> Maybe (Maybe Int) -> Maybe Int -> CM (Maybe CITimed)
|
|
|
|
sndCITimed_ live chatTTL itemTTL =
|
|
|
|
forM (chatTTL >>= (itemTTL <|>)) $ \ttl ->
|
|
|
|
CITimed ttl
|
|
|
|
<$> if live
|
|
|
|
then pure Nothing
|
|
|
|
else Just . addUTCTime (realToFrac ttl) <$> liftIO getCurrentTime
|
|
|
|
|
|
|
|
callTimed :: Contact -> ACIContent -> CM (Maybe CITimed)
|
|
|
|
callTimed ct aciContent =
|
|
|
|
case aciContentCallStatus aciContent of
|
|
|
|
Just callStatus
|
|
|
|
| callComplete callStatus -> do
|
|
|
|
contactCITimed ct
|
|
|
|
_ -> pure Nothing
|
|
|
|
where
|
|
|
|
aciContentCallStatus :: ACIContent -> Maybe CICallStatus
|
|
|
|
aciContentCallStatus (ACIContent _ (CISndCall st _)) = Just st
|
|
|
|
aciContentCallStatus (ACIContent _ (CIRcvCall st _)) = Just st
|
|
|
|
aciContentCallStatus _ = Nothing
|
|
|
|
|
|
|
|
toggleNtf :: User -> GroupMember -> Bool -> CM ()
|
|
|
|
toggleNtf user m ntfOn =
|
|
|
|
when (memberActive m) $
|
|
|
|
forM_ (memberConnId m) $ \connId ->
|
|
|
|
withAgent (\a -> toggleConnectionNtfs a connId ntfOn) `catchChatError` (toView . CRChatError (Just user))
|
|
|
|
|
2025-01-30 10:06:26 +00:00
|
|
|
prepareGroupMsg :: DB.Connection -> User -> GroupInfo -> MsgContent -> Map MemberName MsgMention -> Maybe ChatItemId -> Maybe CIForwardedFrom -> Maybe FileInvitation -> Maybe CITimed -> Bool -> ExceptT StoreError IO (ChatMsgEvent 'Json, Maybe (CIQuote 'CTGroup))
|
|
|
|
prepareGroupMsg db user g@GroupInfo {membership} mc mentions quotedItemId_ itemForwarded fInv_ timed_ live = case (quotedItemId_, itemForwarded) of
|
|
|
|
(Nothing, Nothing) ->
|
|
|
|
let mc' = MCSimple $ ExtMsgContent mc mentions fInv_ (ttl' <$> timed_) (justTrue live)
|
|
|
|
in pure (XMsgNew mc', Nothing)
|
2025-01-29 13:04:48 +00:00
|
|
|
(Nothing, Just _) ->
|
2025-01-30 10:06:26 +00:00
|
|
|
let mc' = MCForward $ ExtMsgContent mc mentions fInv_ (ttl' <$> timed_) (justTrue live)
|
|
|
|
in pure (XMsgNew mc', Nothing)
|
2024-12-20 16:54:24 +04:00
|
|
|
(Just quotedItemId, Nothing) -> do
|
2025-01-30 10:06:26 +00:00
|
|
|
CChatItem _ qci@ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, formattedText, mentions = quoteMentions, file} <-
|
|
|
|
getGroupCIWithReactions db user g quotedItemId
|
2024-12-20 16:54:24 +04:00
|
|
|
(origQmc, qd, sent, GroupMember {memberId}) <- quoteData qci membership
|
|
|
|
let msgRef = MsgRef {msgId = itemSharedMsgId, sentAt = itemTs, sent, memberId = Just memberId}
|
|
|
|
qmc = quoteContent mc origQmc file
|
2025-01-30 10:06:26 +00:00
|
|
|
(qmc', ft', _) = updatedMentionNames qmc formattedText quoteMentions
|
|
|
|
quotedItem = CIQuote {chatDir = qd, itemId = Just quotedItemId, sharedMsgId = itemSharedMsgId, sentAt = itemTs, content = qmc', formattedText = ft'}
|
|
|
|
mc' = MCQuote QuotedMsg {msgRef, content = qmc'} (ExtMsgContent mc mentions fInv_ (ttl' <$> timed_) (justTrue live))
|
|
|
|
pure (XMsgNew mc', Just quotedItem)
|
2025-01-29 13:04:48 +00:00
|
|
|
(Just _, Just _) -> throwError SEInvalidQuote
|
2024-12-20 16:54:24 +04:00
|
|
|
where
|
2025-01-29 13:04:48 +00:00
|
|
|
quoteData :: ChatItem c d -> GroupMember -> ExceptT StoreError IO (MsgContent, CIQDirection 'CTGroup, Bool, GroupMember)
|
|
|
|
quoteData ChatItem {meta = CIMeta {itemDeleted = Just _}} _ = throwError SEInvalidQuote
|
2024-12-20 16:54:24 +04:00
|
|
|
quoteData ChatItem {chatDir = CIGroupSnd, content = CISndMsgContent qmc} membership' = pure (qmc, CIQGroupSnd, True, membership')
|
|
|
|
quoteData ChatItem {chatDir = CIGroupRcv m, content = CIRcvMsgContent qmc} _ = pure (qmc, CIQGroupRcv $ Just m, False, m)
|
2025-01-29 13:04:48 +00:00
|
|
|
quoteData _ _ = throwError SEInvalidQuote
|
|
|
|
|
2025-01-30 10:06:26 +00:00
|
|
|
updatedMentionNames :: MsgContent -> Maybe MarkdownList -> Map MemberName CIMention -> (MsgContent, Maybe MarkdownList, Map MemberName CIMention)
|
|
|
|
updatedMentionNames mc ft_ mentions = case ft_ of
|
2025-02-25 14:05:49 +04:00
|
|
|
Just ft
|
|
|
|
| not (null ft) && not (null mentions) && not (all sameName $ M.assocs mentions) ->
|
|
|
|
let (mentions', ft') = mapAccumL update M.empty ft
|
|
|
|
text = T.concat $ map markdownText ft'
|
|
|
|
in (mc {text} :: MsgContent, Just ft', mentions')
|
2025-01-30 10:06:26 +00:00
|
|
|
_ -> (mc, ft_, mentions)
|
|
|
|
where
|
|
|
|
sameName (name, CIMention {memberRef}) = case memberRef of
|
|
|
|
Just CIMentionMember {displayName} -> case T.stripPrefix displayName name of
|
|
|
|
Just rest
|
|
|
|
| T.null rest -> True
|
|
|
|
| otherwise -> case T.uncons rest of
|
|
|
|
Just ('_', suffix) -> T.all isDigit suffix
|
|
|
|
_ -> False
|
|
|
|
Nothing -> False
|
|
|
|
Nothing -> True
|
|
|
|
update mentions' ft@(FormattedText f _) = case f of
|
|
|
|
Just (Mention name) -> case M.lookup name mentions of
|
|
|
|
Just mm@CIMention {memberRef} ->
|
|
|
|
let name' = uniqueMentionName 0 $ case memberRef of
|
|
|
|
Just CIMentionMember {displayName} -> displayName
|
|
|
|
Nothing -> name
|
|
|
|
in (M.insert name' mm mentions', FormattedText (Just $ Mention name') ('@' `T.cons` viewName name'))
|
|
|
|
Nothing -> (mentions', ft)
|
|
|
|
_ -> (mentions', ft)
|
|
|
|
where
|
|
|
|
uniqueMentionName :: Int -> Text -> Text
|
|
|
|
uniqueMentionName pfx name =
|
|
|
|
let prefixed = if pfx == 0 then name else (name `T.snoc` '_') <> tshow pfx
|
|
|
|
in if prefixed `M.member` mentions' then uniqueMentionName (pfx + 1) name else prefixed
|
|
|
|
|
|
|
|
getCIMentions :: DB.Connection -> User -> GroupInfo -> Maybe MarkdownList -> Map MemberName GroupMemberId -> ExceptT StoreError IO (Map MemberName CIMention)
|
|
|
|
getCIMentions db user GroupInfo {groupId} ft_ mentions = case ft_ of
|
|
|
|
Just ft | not (null ft) && not (null mentions) -> do
|
2025-01-29 13:04:48 +00:00
|
|
|
let msgMentions = S.fromList $ mentionedNames ft
|
|
|
|
n = M.size mentions
|
|
|
|
-- prevent "invisible" and repeated-with-different-name mentions (when the same member is mentioned via another name)
|
|
|
|
unless (n <= maxSndMentions && all (`S.member` msgMentions) (M.keys mentions) && S.size (S.fromList $ M.elems mentions) == n) $
|
|
|
|
throwError SEInvalidMention
|
2025-01-30 10:06:26 +00:00
|
|
|
mapM (getMentionedGroupMember db user groupId) mentions
|
|
|
|
_ -> pure M.empty
|
|
|
|
|
|
|
|
getRcvCIMentions :: DB.Connection -> User -> GroupInfo -> Maybe MarkdownList -> Map MemberName MsgMention -> IO (Map MemberName CIMention)
|
|
|
|
getRcvCIMentions db user GroupInfo {groupId} ft_ mentions = case ft_ of
|
2025-02-25 14:05:49 +04:00
|
|
|
Just ft
|
|
|
|
| not (null ft) && not (null mentions) ->
|
|
|
|
let mentions' = uniqueMsgMentions maxRcvMentions mentions $ mentionedNames ft
|
|
|
|
in mapM (getMentionedMemberByMemberId db user groupId) mentions'
|
2025-01-29 13:04:48 +00:00
|
|
|
_ -> pure M.empty
|
|
|
|
|
|
|
|
-- prevent "invisible" and repeated-with-different-name mentions
|
2025-01-30 10:06:26 +00:00
|
|
|
uniqueMsgMentions :: Int -> Map MemberName MsgMention -> [ContactName] -> Map MemberName MsgMention
|
2025-01-29 13:04:48 +00:00
|
|
|
uniqueMsgMentions maxMentions mentions = go M.empty S.empty 0
|
|
|
|
where
|
|
|
|
go acc _ _ [] = acc
|
|
|
|
go acc seen n (name : rest)
|
|
|
|
| n >= maxMentions = acc
|
|
|
|
| otherwise = case M.lookup name mentions of
|
2025-02-25 14:05:49 +04:00
|
|
|
Just mm@MsgMention {memberId}
|
|
|
|
| S.notMember memberId seen ->
|
|
|
|
go (M.insert name mm acc) (S.insert memberId seen) (n + 1) rest
|
2025-01-29 13:04:48 +00:00
|
|
|
_ -> go acc seen n rest
|
|
|
|
|
|
|
|
getMessageMentions :: DB.Connection -> User -> GroupId -> Text -> IO (Map MemberName GroupMemberId)
|
|
|
|
getMessageMentions db user gId msg = case parseMaybeMarkdownList msg of
|
2025-01-30 10:06:26 +00:00
|
|
|
Just ft | not (null ft) -> M.fromList . catMaybes <$> mapM get (nubOrd $ mentionedNames ft)
|
|
|
|
_ -> pure M.empty
|
2025-01-29 13:04:48 +00:00
|
|
|
where
|
|
|
|
get name =
|
|
|
|
fmap (name,) . eitherToMaybe
|
|
|
|
<$> runExceptT (getGroupMemberIdByName db user gId name)
|
|
|
|
|
|
|
|
msgContentTexts :: MsgContent -> (Text, Maybe MarkdownList)
|
|
|
|
msgContentTexts mc = let t = msgContentText mc in (t, parseMaybeMarkdownList t)
|
|
|
|
|
|
|
|
ciContentTexts :: CIContent d -> (Text, Maybe MarkdownList)
|
|
|
|
ciContentTexts content = let t = ciContentToText content in (t, parseMaybeMarkdownList t)
|
2024-12-20 16:54:24 +04:00
|
|
|
|
|
|
|
quoteContent :: forall d. MsgContent -> MsgContent -> Maybe (CIFile d) -> MsgContent
|
|
|
|
quoteContent mc qmc ciFile_
|
|
|
|
| replaceContent = MCText qTextOrFile
|
|
|
|
| otherwise = case qmc of
|
|
|
|
MCImage _ image -> MCImage qTextOrFile image
|
|
|
|
MCFile _ -> MCFile qTextOrFile
|
|
|
|
-- consider same for voice messages
|
|
|
|
-- MCVoice _ voice -> MCVoice qTextOrFile voice
|
|
|
|
_ -> qmc
|
|
|
|
where
|
|
|
|
-- if the message we're quoting with is one of the "large" MsgContents
|
|
|
|
-- we replace the quote's content with MCText
|
|
|
|
replaceContent = case mc of
|
|
|
|
MCText _ -> False
|
|
|
|
MCFile _ -> False
|
|
|
|
MCLink {} -> True
|
|
|
|
MCImage {} -> True
|
|
|
|
MCVideo {} -> True
|
|
|
|
MCVoice {} -> False
|
2025-01-04 19:17:19 +00:00
|
|
|
MCReport {} -> False
|
2024-12-20 16:54:24 +04:00
|
|
|
MCUnknown {} -> True
|
|
|
|
qText = msgContentText qmc
|
|
|
|
getFileName :: CIFile d -> String
|
|
|
|
getFileName CIFile {fileName} = fileName
|
|
|
|
qFileName = maybe qText (T.pack . getFileName) ciFile_
|
|
|
|
qTextOrFile = if T.null qText then qFileName else qText
|
|
|
|
|
2025-02-10 09:06:16 +00:00
|
|
|
prohibitedGroupContent :: GroupInfo -> GroupMember -> MsgContent -> Maybe MarkdownList -> Maybe f -> Bool -> Maybe GroupFeature
|
|
|
|
prohibitedGroupContent gInfo@GroupInfo {membership = GroupMember {memberRole = userRole}} m mc ft file_ sent
|
2024-12-20 16:54:24 +04:00
|
|
|
| isVoice mc && not (groupFeatureMemberAllowed SGFVoice m gInfo) = Just GFVoice
|
|
|
|
| not (isVoice mc) && isJust file_ && not (groupFeatureMemberAllowed SGFFiles m gInfo) = Just GFFiles
|
2025-02-10 09:06:16 +00:00
|
|
|
| isReport mc && (badReportUser || not (groupFeatureAllowed SGFReports gInfo)) = Just GFReports
|
2025-01-29 13:04:48 +00:00
|
|
|
| prohibitedSimplexLinks gInfo m ft = Just GFSimplexLinks
|
2024-12-20 16:54:24 +04:00
|
|
|
| otherwise = Nothing
|
2025-02-10 09:06:16 +00:00
|
|
|
where
|
|
|
|
-- admins cannot send reports, non-admins cannot receive reports
|
|
|
|
badReportUser
|
|
|
|
| sent = userRole >= GRModerator
|
|
|
|
| otherwise = userRole < GRModerator
|
2024-12-20 16:54:24 +04:00
|
|
|
|
2025-01-29 13:04:48 +00:00
|
|
|
prohibitedSimplexLinks :: GroupInfo -> GroupMember -> Maybe MarkdownList -> Bool
|
|
|
|
prohibitedSimplexLinks gInfo m ft =
|
2024-12-20 16:54:24 +04:00
|
|
|
not (groupFeatureMemberAllowed SGFSimplexLinks m gInfo)
|
2025-01-29 13:04:48 +00:00
|
|
|
&& maybe False (any ftIsSimplexLink) ft
|
2024-12-20 16:54:24 +04:00
|
|
|
where
|
|
|
|
ftIsSimplexLink :: FormattedText -> Bool
|
|
|
|
ftIsSimplexLink FormattedText {format} = maybe False isSimplexLink format
|
|
|
|
|
|
|
|
roundedFDCount :: Int -> Int
|
|
|
|
roundedFDCount n
|
|
|
|
| n <= 0 = 4
|
|
|
|
| otherwise = max 4 $ fromIntegral $ (2 :: Integer) ^ (ceiling (logBase 2 (fromIntegral n) :: Double) :: Integer)
|
|
|
|
|
|
|
|
xftpSndFileTransfer_ :: User -> CryptoFile -> Integer -> Int -> Maybe ContactOrGroup -> CM (FileInvitation, CIFile 'MDSnd, FileTransferMeta)
|
|
|
|
xftpSndFileTransfer_ user file@(CryptoFile filePath cfArgs) fileSize n contactOrGroup_ = do
|
|
|
|
let fileName = takeFileName filePath
|
|
|
|
fInv = xftpFileInvitation fileName fileSize dummyFileDescr
|
|
|
|
fsFilePath <- lift $ toFSFilePath filePath
|
|
|
|
let srcFile = CryptoFile fsFilePath cfArgs
|
|
|
|
aFileId <- withAgent $ \a -> xftpSendFile a (aUserId user) srcFile (roundedFDCount n)
|
|
|
|
-- TODO CRSndFileStart event for XFTP
|
|
|
|
chSize <- asks $ fileChunkSize . config
|
|
|
|
ft@FileTransferMeta {fileId} <- withStore' $ \db -> createSndFileTransferXFTP db user contactOrGroup_ file fInv (AgentSndFileId aFileId) Nothing chSize
|
|
|
|
let fileSource = Just $ CryptoFile filePath cfArgs
|
|
|
|
ciFile = CIFile {fileId, fileName, fileSize, fileSource, fileStatus = CIFSSndStored, fileProtocol = FPXFTP}
|
|
|
|
pure (fInv, ciFile, ft)
|
|
|
|
|
|
|
|
xftpSndFileRedirect :: User -> FileTransferId -> ValidFileDescription 'FRecipient -> CM FileTransferMeta
|
|
|
|
xftpSndFileRedirect user ftId vfd = do
|
|
|
|
let fileName = "redirect.yaml"
|
|
|
|
file = CryptoFile fileName Nothing
|
|
|
|
fInv = xftpFileInvitation fileName (fromIntegral $ B.length $ strEncode vfd) dummyFileDescr
|
|
|
|
aFileId <- withAgent $ \a -> xftpSendDescription a (aUserId user) vfd (roundedFDCount 1)
|
|
|
|
chSize <- asks $ fileChunkSize . config
|
|
|
|
withStore' $ \db -> createSndFileTransferXFTP db user Nothing file fInv (AgentSndFileId aFileId) (Just ftId) chSize
|
|
|
|
|
|
|
|
dummyFileDescr :: FileDescr
|
|
|
|
dummyFileDescr = FileDescr {fileDescrText = "", fileDescrPartNo = 0, fileDescrComplete = False}
|
|
|
|
|
|
|
|
cancelFilesInProgress :: User -> [CIFileInfo] -> CM ()
|
|
|
|
cancelFilesInProgress user filesInfo = do
|
|
|
|
let filesInfo' = filter (not . fileEnded) filesInfo
|
|
|
|
(sfs, rfs) <- lift $ splitFTTypes <$> withStoreBatch (\db -> map (getFT db) filesInfo')
|
|
|
|
forM_ rfs $ \RcvFileTransfer {fileId} -> lift (closeFileHandle fileId rcvFiles) `catchChatError` \_ -> pure ()
|
|
|
|
lift . void . withStoreBatch' $ \db -> map (updateSndFileCancelled db) sfs
|
|
|
|
lift . void . withStoreBatch' $ \db -> map (updateRcvFileCancelled db) rfs
|
|
|
|
let xsfIds = mapMaybe (\(FileTransferMeta {fileId, xftpSndFile}, _) -> (,fileId) <$> xftpSndFile) sfs
|
|
|
|
xrfIds = mapMaybe (\RcvFileTransfer {fileId, xftpRcvFile} -> (,fileId) <$> xftpRcvFile) rfs
|
|
|
|
lift $ agentXFTPDeleteSndFilesRemote user xsfIds
|
|
|
|
lift $ agentXFTPDeleteRcvFiles xrfIds
|
|
|
|
let smpSFConnIds = concatMap (\(ft, sfts) -> mapMaybe (smpSndFileConnId ft) sfts) sfs
|
|
|
|
smpRFConnIds = mapMaybe smpRcvFileConnId rfs
|
|
|
|
deleteAgentConnectionsAsync user smpSFConnIds
|
|
|
|
deleteAgentConnectionsAsync user smpRFConnIds
|
|
|
|
where
|
|
|
|
fileEnded CIFileInfo {fileStatus} = case fileStatus of
|
|
|
|
Just (AFS _ status) -> ciFileEnded status
|
|
|
|
Nothing -> True
|
|
|
|
getFT :: DB.Connection -> CIFileInfo -> IO (Either ChatError FileTransfer)
|
|
|
|
getFT db CIFileInfo {fileId} = runExceptT . withExceptT ChatErrorStore $ getFileTransfer db user fileId
|
|
|
|
updateSndFileCancelled :: DB.Connection -> (FileTransferMeta, [SndFileTransfer]) -> IO ()
|
|
|
|
updateSndFileCancelled db (FileTransferMeta {fileId}, sfts) = do
|
|
|
|
updateFileCancelled db user fileId CIFSSndCancelled
|
|
|
|
forM_ sfts updateSndFTCancelled
|
|
|
|
where
|
|
|
|
updateSndFTCancelled :: SndFileTransfer -> IO ()
|
|
|
|
updateSndFTCancelled ft = unless (sndFTEnded ft) $ do
|
|
|
|
updateSndFileStatus db ft FSCancelled
|
|
|
|
deleteSndFileChunks db ft
|
|
|
|
updateRcvFileCancelled :: DB.Connection -> RcvFileTransfer -> IO ()
|
|
|
|
updateRcvFileCancelled db ft@RcvFileTransfer {fileId} = do
|
|
|
|
updateFileCancelled db user fileId CIFSRcvCancelled
|
|
|
|
updateRcvFileStatus db fileId FSCancelled
|
|
|
|
deleteRcvFileChunks db ft
|
|
|
|
splitFTTypes :: [Either ChatError FileTransfer] -> ([(FileTransferMeta, [SndFileTransfer])], [RcvFileTransfer])
|
|
|
|
splitFTTypes = foldr addFT ([], []) . rights
|
|
|
|
where
|
|
|
|
addFT f (sfs, rfs) = case f of
|
|
|
|
FTSnd ft@FileTransferMeta {cancelled} sfts | not cancelled -> ((ft, sfts) : sfs, rfs)
|
|
|
|
FTRcv ft@RcvFileTransfer {cancelled} | not cancelled -> (sfs, ft : rfs)
|
|
|
|
_ -> (sfs, rfs)
|
|
|
|
smpSndFileConnId :: FileTransferMeta -> SndFileTransfer -> Maybe ConnId
|
|
|
|
smpSndFileConnId FileTransferMeta {xftpSndFile} sft@SndFileTransfer {agentConnId = AgentConnId acId, fileInline}
|
|
|
|
| isNothing xftpSndFile && isNothing fileInline && not (sndFTEnded sft) = Just acId
|
|
|
|
| otherwise = Nothing
|
|
|
|
smpRcvFileConnId :: RcvFileTransfer -> Maybe ConnId
|
|
|
|
smpRcvFileConnId ft@RcvFileTransfer {xftpRcvFile, rcvFileInline}
|
|
|
|
| isNothing xftpRcvFile && isNothing rcvFileInline = liveRcvFileTransferConnId ft
|
|
|
|
| otherwise = Nothing
|
|
|
|
sndFTEnded SndFileTransfer {fileStatus} = fileStatus == FSCancelled || fileStatus == FSComplete
|
|
|
|
|
|
|
|
deleteFilesLocally :: [CIFileInfo] -> CM ()
|
|
|
|
deleteFilesLocally files =
|
|
|
|
withFilesFolder $ \filesFolder ->
|
|
|
|
liftIO . forM_ files $ \CIFileInfo {filePath} ->
|
|
|
|
mapM_ (delete . (filesFolder </>)) filePath
|
|
|
|
where
|
|
|
|
delete :: FilePath -> IO ()
|
|
|
|
delete fPath =
|
|
|
|
removeFile fPath `catchAll` \_ ->
|
|
|
|
removePathForcibly fPath `catchAll_` pure ()
|
|
|
|
-- perform an action only if filesFolder is set (i.e. on mobile devices)
|
|
|
|
withFilesFolder :: (FilePath -> CM ()) -> CM ()
|
|
|
|
withFilesFolder action = asks filesFolder >>= readTVarIO >>= mapM_ action
|
|
|
|
|
|
|
|
deleteDirectCIs :: User -> Contact -> [CChatItem 'CTDirect] -> Bool -> Bool -> CM ChatResponse
|
|
|
|
deleteDirectCIs user ct items byUser timed = do
|
|
|
|
let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items
|
|
|
|
deleteCIFiles user ciFilesInfo
|
|
|
|
(errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (deleteItem db) items)
|
|
|
|
unless (null errs) $ toView $ CRChatErrors (Just user) errs
|
|
|
|
pure $ CRChatItemsDeleted user deletions byUser timed
|
|
|
|
where
|
|
|
|
deleteItem db (CChatItem md ci) = do
|
|
|
|
deleteDirectChatItem db user ct ci
|
|
|
|
pure $ contactDeletion md ct ci Nothing
|
|
|
|
|
|
|
|
deleteGroupCIs :: User -> GroupInfo -> [CChatItem 'CTGroup] -> Bool -> Bool -> Maybe GroupMember -> UTCTime -> CM ChatResponse
|
|
|
|
deleteGroupCIs user gInfo items byUser timed byGroupMember_ deletedTs = do
|
|
|
|
let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items
|
|
|
|
deleteCIFiles user ciFilesInfo
|
|
|
|
(errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (deleteItem db) items)
|
|
|
|
unless (null errs) $ toView $ CRChatErrors (Just user) errs
|
|
|
|
pure $ CRChatItemsDeleted user deletions byUser timed
|
|
|
|
where
|
|
|
|
deleteItem :: DB.Connection -> CChatItem 'CTGroup -> IO ChatItemDeletion
|
|
|
|
deleteItem db (CChatItem md ci) = do
|
|
|
|
ci' <- case byGroupMember_ of
|
|
|
|
Just m -> Just <$> updateGroupChatItemModerated db user gInfo ci m deletedTs
|
|
|
|
Nothing -> Nothing <$ deleteGroupChatItem db user gInfo ci
|
|
|
|
pure $ groupDeletion md gInfo ci ci'
|
|
|
|
|
|
|
|
deleteLocalCIs :: User -> NoteFolder -> [CChatItem 'CTLocal] -> Bool -> Bool -> CM ChatResponse
|
|
|
|
deleteLocalCIs user nf items byUser timed = do
|
|
|
|
let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items
|
|
|
|
deleteFilesLocally ciFilesInfo
|
|
|
|
(errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (deleteItem db) items)
|
|
|
|
unless (null errs) $ toView $ CRChatErrors (Just user) errs
|
|
|
|
pure $ CRChatItemsDeleted user deletions byUser timed
|
|
|
|
where
|
|
|
|
deleteItem db (CChatItem md ci) = do
|
|
|
|
deleteLocalChatItem db user nf ci
|
|
|
|
pure $ ChatItemDeletion (nfItem md ci) Nothing
|
|
|
|
nfItem :: MsgDirectionI d => SMsgDirection d -> ChatItem 'CTLocal d -> AChatItem
|
|
|
|
nfItem md = AChatItem SCTLocal md (LocalChat nf)
|
|
|
|
|
|
|
|
deleteCIFiles :: User -> [CIFileInfo] -> CM ()
|
|
|
|
deleteCIFiles user filesInfo = do
|
|
|
|
cancelFilesInProgress user filesInfo
|
|
|
|
deleteFilesLocally filesInfo
|
|
|
|
|
|
|
|
markDirectCIsDeleted :: User -> Contact -> [CChatItem 'CTDirect] -> Bool -> UTCTime -> CM ChatResponse
|
|
|
|
markDirectCIsDeleted user ct items byUser deletedTs = do
|
|
|
|
let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items
|
|
|
|
cancelFilesInProgress user ciFilesInfo
|
|
|
|
(errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (markDeleted db) items)
|
|
|
|
unless (null errs) $ toView $ CRChatErrors (Just user) errs
|
|
|
|
pure $ CRChatItemsDeleted user deletions byUser False
|
|
|
|
where
|
|
|
|
markDeleted db (CChatItem md ci) = do
|
|
|
|
ci' <- markDirectChatItemDeleted db user ct ci deletedTs
|
|
|
|
pure $ contactDeletion md ct ci (Just ci')
|
|
|
|
|
|
|
|
markGroupCIsDeleted :: User -> GroupInfo -> [CChatItem 'CTGroup] -> Bool -> Maybe GroupMember -> UTCTime -> CM ChatResponse
|
|
|
|
markGroupCIsDeleted user gInfo items byUser byGroupMember_ deletedTs = do
|
|
|
|
let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items
|
|
|
|
cancelFilesInProgress user ciFilesInfo
|
|
|
|
(errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (markDeleted db) items)
|
|
|
|
unless (null errs) $ toView $ CRChatErrors (Just user) errs
|
|
|
|
pure $ CRChatItemsDeleted user deletions byUser False
|
|
|
|
where
|
|
|
|
markDeleted db (CChatItem md ci) = do
|
|
|
|
ci' <- markGroupChatItemDeleted db user gInfo ci byGroupMember_ deletedTs
|
|
|
|
pure $ groupDeletion md gInfo ci (Just ci')
|
|
|
|
|
|
|
|
groupDeletion :: MsgDirectionI d => SMsgDirection d -> GroupInfo -> ChatItem 'CTGroup d -> Maybe (ChatItem 'CTGroup d) -> ChatItemDeletion
|
|
|
|
groupDeletion md g ci ci' = ChatItemDeletion (gItem ci) (gItem <$> ci')
|
|
|
|
where
|
|
|
|
gItem = AChatItem SCTGroup md (GroupChat g)
|
|
|
|
|
|
|
|
contactDeletion :: MsgDirectionI d => SMsgDirection d -> Contact -> ChatItem 'CTDirect d -> Maybe (ChatItem 'CTDirect d) -> ChatItemDeletion
|
|
|
|
contactDeletion md ct ci ci' = ChatItemDeletion (ctItem ci) (ctItem <$> ci')
|
|
|
|
where
|
|
|
|
ctItem = AChatItem SCTDirect md (DirectChat ct)
|
|
|
|
|
|
|
|
updateCallItemStatus :: User -> Contact -> Call -> WebRTCCallStatus -> Maybe MessageId -> CM ()
|
|
|
|
updateCallItemStatus user ct@Contact {contactId} Call {chatItemId} receivedStatus msgId_ = do
|
|
|
|
aciContent_ <- callStatusItemContent user ct chatItemId receivedStatus
|
|
|
|
forM_ aciContent_ $ \aciContent -> do
|
|
|
|
timed_ <- callTimed ct aciContent
|
|
|
|
updateDirectChatItemView user ct chatItemId aciContent False False timed_ msgId_
|
|
|
|
forM_ (timed_ >>= timedDeleteAt') $
|
|
|
|
startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId)
|
|
|
|
|
|
|
|
updateDirectChatItemView :: User -> Contact -> ChatItemId -> ACIContent -> Bool -> Bool -> Maybe CITimed -> Maybe MessageId -> CM ()
|
|
|
|
updateDirectChatItemView user ct chatItemId (ACIContent msgDir ciContent) edited live timed_ msgId_ = do
|
|
|
|
ci' <- withStore $ \db -> updateDirectChatItem db user ct chatItemId ciContent edited live timed_ msgId_
|
|
|
|
toView $ CRChatItemUpdated user (AChatItem SCTDirect msgDir (DirectChat ct) ci')
|
|
|
|
|
|
|
|
callStatusItemContent :: User -> Contact -> ChatItemId -> WebRTCCallStatus -> CM (Maybe ACIContent)
|
|
|
|
callStatusItemContent user Contact {contactId} chatItemId receivedStatus = do
|
|
|
|
CChatItem msgDir ChatItem {meta = CIMeta {updatedAt}, content} <-
|
|
|
|
withStore $ \db -> getDirectChatItem db user contactId chatItemId
|
|
|
|
ts <- liftIO getCurrentTime
|
|
|
|
let callDuration :: Int = nominalDiffTimeToSeconds (ts `diffUTCTime` updatedAt) `div'` 1
|
|
|
|
callStatus = case content of
|
|
|
|
CISndCall st _ -> Just st
|
|
|
|
CIRcvCall st _ -> Just st
|
|
|
|
_ -> Nothing
|
|
|
|
newState_ = case (callStatus, receivedStatus) of
|
|
|
|
(Just CISCallProgress, WCSConnected) -> Nothing -- if call in-progress received connected -> no change
|
|
|
|
(Just CISCallProgress, WCSDisconnected) -> Just (CISCallEnded, callDuration) -- calculate in-progress duration
|
|
|
|
(Just CISCallProgress, WCSFailed) -> Just (CISCallEnded, callDuration) -- whether call disconnected or failed
|
|
|
|
(Just CISCallPending, WCSDisconnected) -> Just (CISCallMissed, 0)
|
|
|
|
(Just CISCallEnded, _) -> Nothing -- if call already ended or failed -> no change
|
|
|
|
(Just CISCallError, _) -> Nothing
|
|
|
|
(Just _, WCSConnecting) -> Just (CISCallNegotiated, 0)
|
|
|
|
(Just _, WCSConnected) -> Just (CISCallProgress, 0) -- if call ended that was never connected, duration = 0
|
|
|
|
(Just _, WCSDisconnected) -> Just (CISCallEnded, 0)
|
|
|
|
(Just _, WCSFailed) -> Just (CISCallError, 0)
|
|
|
|
(Nothing, _) -> Nothing -- some other content - we should never get here, but no exception is thrown
|
|
|
|
pure $ aciContent msgDir <$> newState_
|
|
|
|
where
|
|
|
|
aciContent :: forall d. SMsgDirection d -> (CICallStatus, Int) -> ACIContent
|
|
|
|
aciContent msgDir (callStatus', duration) = case msgDir of
|
|
|
|
SMDSnd -> ACIContent SMDSnd $ CISndCall callStatus' duration
|
|
|
|
SMDRcv -> ACIContent SMDRcv $ CIRcvCall callStatus' duration
|
|
|
|
|
|
|
|
-- mobile clients use file paths relative to app directory (e.g. for the reason ios app directory changes on updates),
|
|
|
|
-- so we have to differentiate between the file path stored in db and communicated with frontend, and the file path
|
|
|
|
-- used during file transfer for actual operations with file system
|
|
|
|
toFSFilePath :: FilePath -> CM' FilePath
|
|
|
|
toFSFilePath f =
|
|
|
|
maybe f (</> f) <$> (chatReadVar' filesFolder)
|
|
|
|
|
|
|
|
setFileToEncrypt :: RcvFileTransfer -> CM RcvFileTransfer
|
|
|
|
setFileToEncrypt ft@RcvFileTransfer {fileId} = do
|
|
|
|
cfArgs <- atomically . CF.randomArgs =<< asks random
|
|
|
|
withStore' $ \db -> setFileCryptoArgs db fileId cfArgs
|
|
|
|
pure (ft :: RcvFileTransfer) {cryptoArgs = Just cfArgs}
|
|
|
|
|
|
|
|
receiveFile' :: User -> RcvFileTransfer -> Bool -> Maybe Bool -> Maybe FilePath -> CM ChatResponse
|
|
|
|
receiveFile' user ft userApprovedRelays rcvInline_ filePath_ = do
|
|
|
|
(CRRcvFileAccepted user <$> acceptFileReceive user ft userApprovedRelays rcvInline_ filePath_) `catchChatError` processError
|
|
|
|
where
|
|
|
|
processError = \case
|
|
|
|
-- TODO AChatItem in Cancelled events
|
|
|
|
ChatErrorAgent (SMP _ SMP.AUTH) _ -> pure $ CRRcvFileAcceptedSndCancelled user ft
|
|
|
|
ChatErrorAgent (CONN DUPLICATE) _ -> pure $ CRRcvFileAcceptedSndCancelled user ft
|
|
|
|
e -> throwError e
|
|
|
|
|
|
|
|
acceptFileReceive :: User -> RcvFileTransfer -> Bool -> Maybe Bool -> Maybe FilePath -> CM AChatItem
|
|
|
|
acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = FileInvitation {fileName = fName, fileConnReq, fileInline, fileSize}, fileStatus, grpMemberId, cryptoArgs} userApprovedRelays rcvInline_ filePath_ = do
|
|
|
|
unless (fileStatus == RFSNew) $ case fileStatus of
|
|
|
|
RFSCancelled _ -> throwChatError $ CEFileCancelled fName
|
|
|
|
_ -> throwChatError $ CEFileAlreadyReceiving fName
|
|
|
|
vr <- chatVersionRange
|
|
|
|
case (xftpRcvFile, fileConnReq) of
|
|
|
|
-- direct file protocol
|
|
|
|
(Nothing, Just connReq) -> do
|
|
|
|
subMode <- chatReadVar subscriptionMode
|
|
|
|
dm <- encodeConnInfo $ XFileAcpt fName
|
|
|
|
connIds <- joinAgentConnectionAsync user True connReq dm subMode
|
|
|
|
filePath <- getRcvFilePath fileId filePath_ fName True
|
|
|
|
withStore $ \db -> acceptRcvFileTransfer db vr user fileId connIds ConnJoined filePath subMode
|
|
|
|
-- XFTP
|
|
|
|
(Just XFTPRcvFile {userApprovedRelays = approvedBeforeReady}, _) -> do
|
|
|
|
let userApproved = approvedBeforeReady || userApprovedRelays
|
|
|
|
filePath <- getRcvFilePath fileId filePath_ fName False
|
|
|
|
(ci, rfd) <- withStore $ \db -> do
|
|
|
|
-- marking file as accepted and reading description in the same transaction
|
|
|
|
-- to prevent race condition with appending description
|
|
|
|
ci <- xftpAcceptRcvFT db vr user fileId filePath userApproved
|
|
|
|
rfd <- getRcvFileDescrByRcvFileId db fileId
|
|
|
|
pure (ci, rfd)
|
|
|
|
receiveViaCompleteFD user fileId rfd userApproved cryptoArgs
|
|
|
|
pure ci
|
|
|
|
-- group & direct file protocol
|
|
|
|
_ -> do
|
|
|
|
chatRef <- withStore $ \db -> getChatRefByFileId db user fileId
|
|
|
|
case (chatRef, grpMemberId) of
|
|
|
|
(ChatRef CTDirect contactId, Nothing) -> do
|
|
|
|
ct <- withStore $ \db -> getContact db vr user contactId
|
|
|
|
acceptFile CFCreateConnFileInvDirect $ \msg -> void $ sendDirectContactMessage user ct msg
|
|
|
|
(ChatRef CTGroup groupId, Just memId) -> do
|
|
|
|
GroupMember {activeConn} <- withStore $ \db -> getGroupMember db vr user groupId memId
|
|
|
|
case activeConn of
|
|
|
|
Just conn -> do
|
|
|
|
acceptFile CFCreateConnFileInvGroup $ \msg -> void $ sendDirectMemberMessage conn msg groupId
|
|
|
|
_ -> throwChatError $ CEFileInternal "member connection not active"
|
|
|
|
_ -> throwChatError $ CEFileInternal "invalid chat ref for file transfer"
|
|
|
|
where
|
|
|
|
acceptFile :: CommandFunction -> (ChatMsgEvent 'Json -> CM ()) -> CM AChatItem
|
|
|
|
acceptFile cmdFunction send = do
|
|
|
|
filePath <- getRcvFilePath fileId filePath_ fName True
|
|
|
|
inline <- receiveInline
|
|
|
|
vr <- chatVersionRange
|
|
|
|
if
|
|
|
|
| inline -> do
|
|
|
|
-- accepting inline
|
|
|
|
ci <- withStore $ \db -> acceptRcvInlineFT db vr user fileId filePath
|
|
|
|
sharedMsgId <- withStore $ \db -> getSharedMsgIdByFileId db userId fileId
|
|
|
|
send $ XFileAcptInv sharedMsgId Nothing fName
|
|
|
|
pure ci
|
|
|
|
| fileInline == Just IFMSent -> throwChatError $ CEFileAlreadyReceiving fName
|
|
|
|
| otherwise -> do
|
|
|
|
-- accepting via a new connection
|
|
|
|
subMode <- chatReadVar subscriptionMode
|
|
|
|
connIds <- createAgentConnectionAsync user cmdFunction True SCMInvitation subMode
|
|
|
|
withStore $ \db -> acceptRcvFileTransfer db vr user fileId connIds ConnNew filePath subMode
|
|
|
|
receiveInline :: CM Bool
|
|
|
|
receiveInline = do
|
|
|
|
ChatConfig {fileChunkSize, inlineFiles = InlineFilesConfig {receiveChunks, offerChunks}} <- asks config
|
|
|
|
pure $
|
|
|
|
rcvInline_ /= Just False
|
|
|
|
&& fileInline == Just IFMOffer
|
|
|
|
&& ( fileSize <= fileChunkSize * receiveChunks
|
|
|
|
|| (rcvInline_ == Just True && fileSize <= fileChunkSize * offerChunks)
|
|
|
|
)
|
|
|
|
|
|
|
|
receiveViaCompleteFD :: User -> FileTransferId -> RcvFileDescr -> Bool -> Maybe CryptoFileArgs -> CM ()
|
|
|
|
receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} userApprovedRelays cfArgs =
|
|
|
|
when fileDescrComplete $ do
|
|
|
|
rd <- parseFileDescription fileDescrText
|
|
|
|
if userApprovedRelays
|
|
|
|
then receive' rd True
|
|
|
|
else do
|
|
|
|
let srvs = fileServers rd
|
|
|
|
unknownSrvs <- getUnknownSrvs srvs
|
|
|
|
let approved = null unknownSrvs
|
|
|
|
ifM
|
|
|
|
((approved ||) <$> ipProtectedForSrvs srvs)
|
|
|
|
(receive' rd approved)
|
|
|
|
(relaysNotApproved unknownSrvs)
|
|
|
|
where
|
|
|
|
receive' :: ValidFileDescription 'FRecipient -> Bool -> CM ()
|
|
|
|
receive' rd approved = do
|
|
|
|
aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) rd cfArgs approved
|
|
|
|
startReceivingFile user fileId
|
|
|
|
withStore' $ \db -> updateRcvFileAgentId db fileId (Just $ AgentRcvFileId aFileId)
|
|
|
|
fileServers :: ValidFileDescription 'FRecipient -> [XFTPServer]
|
|
|
|
fileServers (FD.ValidFileDescription FD.FileDescription {chunks}) =
|
|
|
|
S.toList $ S.fromList $ concatMap (\FD.FileChunk {replicas} -> map (\FD.FileChunkReplica {server} -> server) replicas) chunks
|
|
|
|
getUnknownSrvs :: [XFTPServer] -> CM [XFTPServer]
|
|
|
|
getUnknownSrvs srvs = do
|
|
|
|
knownSrvs <- L.map protoServer' <$> getKnownAgentServers SPXFTP user
|
|
|
|
pure $ filter (`notElem` knownSrvs) srvs
|
|
|
|
ipProtectedForSrvs :: [XFTPServer] -> CM Bool
|
|
|
|
ipProtectedForSrvs srvs = do
|
|
|
|
netCfg <- lift getNetworkConfig
|
|
|
|
pure $ all (ipAddressProtected netCfg) srvs
|
|
|
|
relaysNotApproved :: [XFTPServer] -> CM ()
|
|
|
|
relaysNotApproved unknownSrvs = do
|
|
|
|
aci_ <- resetRcvCIFileStatus user fileId CIFSRcvInvitation
|
|
|
|
forM_ aci_ $ \aci -> do
|
|
|
|
cleanupACIFile aci
|
|
|
|
toView $ CRChatItemUpdated user aci
|
|
|
|
throwChatError $ CEFileNotApproved fileId unknownSrvs
|
|
|
|
|
|
|
|
cleanupACIFile :: AChatItem -> CM ()
|
|
|
|
cleanupACIFile (AChatItem _ _ _ ChatItem {file = Just CIFile {fileSource = Just CryptoFile {filePath}}}) = do
|
|
|
|
fsFilePath <- lift $ toFSFilePath filePath
|
|
|
|
removeFile fsFilePath `catchChatError` \_ -> pure ()
|
|
|
|
cleanupACIFile _ = pure ()
|
|
|
|
|
|
|
|
getKnownAgentServers :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> User -> CM (NonEmpty (ServerCfg p))
|
|
|
|
getKnownAgentServers p user = do
|
|
|
|
as <- asks randomAgentServers
|
|
|
|
withStore $ \db -> do
|
|
|
|
opDomains <- operatorDomains . serverOperators <$> getServerOperators db
|
|
|
|
srvs <- liftIO $ getProtocolServers db p user
|
|
|
|
pure $ useServerCfgs p as opDomains srvs
|
|
|
|
|
|
|
|
protoServer' :: ServerCfg p -> ProtocolServer p
|
|
|
|
protoServer' ServerCfg {server} = protoServer server
|
|
|
|
|
|
|
|
getNetworkConfig :: CM' NetworkConfig
|
|
|
|
getNetworkConfig = withAgent' $ liftIO . getFastNetworkConfig
|
|
|
|
|
|
|
|
resetRcvCIFileStatus :: User -> FileTransferId -> CIFileStatus 'MDRcv -> CM (Maybe AChatItem)
|
|
|
|
resetRcvCIFileStatus user fileId ciFileStatus = do
|
|
|
|
vr <- chatVersionRange
|
|
|
|
withStore $ \db -> do
|
|
|
|
liftIO $ do
|
|
|
|
updateCIFileStatus db user fileId ciFileStatus
|
|
|
|
updateRcvFileStatus db fileId FSNew
|
|
|
|
updateRcvFileAgentId db fileId Nothing
|
|
|
|
lookupChatItemByFileId db vr user fileId
|
|
|
|
|
|
|
|
receiveViaURI :: User -> FileDescriptionURI -> CryptoFile -> CM RcvFileTransfer
|
|
|
|
receiveViaURI user@User {userId} FileDescriptionURI {description} cf@CryptoFile {cryptoArgs} = do
|
|
|
|
fileId <- withStore $ \db -> createRcvStandaloneFileTransfer db userId cf fileSize chunkSize
|
|
|
|
-- currently the only use case is user migrating via their configured servers, so we pass approvedRelays = True
|
|
|
|
aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) description cryptoArgs True
|
|
|
|
withStore $ \db -> do
|
|
|
|
liftIO $ do
|
|
|
|
updateRcvFileStatus db fileId FSConnected
|
|
|
|
updateCIFileStatus db user fileId $ CIFSRcvTransfer 0 1
|
|
|
|
updateRcvFileAgentId db fileId (Just $ AgentRcvFileId aFileId)
|
|
|
|
getRcvFileTransfer db user fileId
|
|
|
|
where
|
|
|
|
FD.ValidFileDescription FD.FileDescription {size = FD.FileSize fileSize, chunkSize = FD.FileSize chunkSize} = description
|
|
|
|
|
|
|
|
startReceivingFile :: User -> FileTransferId -> CM ()
|
|
|
|
startReceivingFile user fileId = do
|
|
|
|
vr <- chatVersionRange
|
|
|
|
ci <- withStore $ \db -> do
|
|
|
|
liftIO $ updateRcvFileStatus db fileId FSConnected
|
|
|
|
liftIO $ updateCIFileStatus db user fileId $ CIFSRcvTransfer 0 1
|
|
|
|
getChatItemByFileId db vr user fileId
|
|
|
|
toView $ CRRcvFileStart user ci
|
|
|
|
|
|
|
|
getRcvFilePath :: FileTransferId -> Maybe FilePath -> String -> Bool -> CM FilePath
|
|
|
|
getRcvFilePath fileId fPath_ fn keepHandle = case fPath_ of
|
|
|
|
Nothing ->
|
|
|
|
chatReadVar filesFolder >>= \case
|
|
|
|
Nothing -> do
|
|
|
|
defaultFolder <- lift getDefaultFilesFolder
|
|
|
|
fPath <- liftIO $ defaultFolder `uniqueCombine` fn
|
|
|
|
createEmptyFile fPath $> fPath
|
|
|
|
Just filesFolder -> do
|
|
|
|
fPath <- liftIO $ filesFolder `uniqueCombine` fn
|
|
|
|
createEmptyFile fPath
|
|
|
|
pure $ takeFileName fPath
|
|
|
|
Just fPath ->
|
|
|
|
ifM
|
|
|
|
(doesDirectoryExist fPath)
|
|
|
|
(createInPassedDirectory fPath)
|
|
|
|
$ ifM
|
|
|
|
(doesFileExist fPath)
|
|
|
|
(throwChatError $ CEFileAlreadyExists fPath)
|
|
|
|
(createEmptyFile fPath $> fPath)
|
|
|
|
where
|
|
|
|
createInPassedDirectory :: FilePath -> CM FilePath
|
|
|
|
createInPassedDirectory fPathDir = do
|
|
|
|
fPath <- liftIO $ fPathDir `uniqueCombine` fn
|
|
|
|
createEmptyFile fPath $> fPath
|
|
|
|
createEmptyFile :: FilePath -> CM ()
|
|
|
|
createEmptyFile fPath = emptyFile `catchThrow` (ChatError . CEFileWrite fPath . show)
|
|
|
|
where
|
|
|
|
emptyFile :: CM ()
|
|
|
|
emptyFile
|
|
|
|
| keepHandle = do
|
|
|
|
h <- getFileHandle fileId fPath rcvFiles AppendMode
|
|
|
|
liftIO $ B.hPut h "" >> hFlush h
|
|
|
|
| otherwise = liftIO $ B.writeFile fPath ""
|
|
|
|
|
|
|
|
acceptContactRequest :: User -> UserContactRequest -> IncognitoEnabled -> CM (Contact, Connection, SndQueueSecured)
|
|
|
|
acceptContactRequest user@User {userId} UserContactRequest {agentInvitationId = AgentInvId invId, contactId_, cReqChatVRange, localDisplayName = cName, profileId, profile = cp, userContactLinkId, xContactId, pqSupport} incognito = do
|
|
|
|
subMode <- chatReadVar subscriptionMode
|
|
|
|
let pqSup = PQSupportOn
|
|
|
|
pqSup' = pqSup `CR.pqSupportAnd` pqSupport
|
|
|
|
vr <- chatVersionRange
|
|
|
|
let chatV = vr `peerConnChatVersion` cReqChatVRange
|
|
|
|
(ct, conn, incognitoProfile) <- case contactId_ of
|
|
|
|
Nothing -> do
|
|
|
|
incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing
|
|
|
|
connId <- withAgent $ \a -> prepareConnectionToAccept a True invId pqSup'
|
|
|
|
(ct, conn) <- withStore' $ \db -> createAcceptedContact db user connId chatV cReqChatVRange cName profileId cp userContactLinkId xContactId incognitoProfile subMode pqSup' False
|
|
|
|
pure (ct, conn, incognitoProfile)
|
|
|
|
Just contactId -> do
|
|
|
|
ct <- withFastStore $ \db -> getContact db vr user contactId
|
|
|
|
case contactConn ct of
|
|
|
|
Nothing -> throwChatError $ CECommandError "contact has no connection"
|
|
|
|
Just conn@Connection {customUserProfileId} -> do
|
|
|
|
incognitoProfile <- forM customUserProfileId $ \pId -> withFastStore $ \db -> getProfileById db userId pId
|
|
|
|
pure (ct, conn, ExistingIncognito <$> incognitoProfile)
|
|
|
|
let profileToSend = profileToSendOnAccept user incognitoProfile False
|
|
|
|
dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend
|
|
|
|
(ct,conn,) <$> withAgent (\a -> acceptContact a (aConnId conn) True invId dm pqSup' subMode)
|
|
|
|
|
2025-01-31 10:32:07 +04:00
|
|
|
acceptContactRequestAsync :: User -> UserContactRequest -> Maybe IncognitoProfile -> PQSupport -> CM Contact
|
|
|
|
acceptContactRequestAsync user cReq@UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange, localDisplayName = cName, profileId, profile = p, userContactLinkId, xContactId} incognitoProfile pqSup = do
|
2024-12-20 16:54:24 +04:00
|
|
|
subMode <- chatReadVar subscriptionMode
|
|
|
|
let profileToSend = profileToSendOnAccept user incognitoProfile False
|
|
|
|
vr <- chatVersionRange
|
|
|
|
let chatV = vr `peerConnChatVersion` cReqChatVRange
|
|
|
|
(cmdId, acId) <- agentAcceptContactAsync user True invId (XInfo profileToSend) subMode pqSup chatV
|
|
|
|
withStore' $ \db -> do
|
2025-01-31 10:32:07 +04:00
|
|
|
(ct, Connection {connId}) <- createAcceptedContact db user acId chatV cReqChatVRange cName profileId p userContactLinkId xContactId incognitoProfile subMode pqSup True
|
2024-12-20 16:54:24 +04:00
|
|
|
deleteContactRequestRec db user cReq
|
|
|
|
setCommandConnId db user cmdId connId
|
|
|
|
pure ct
|
|
|
|
|
|
|
|
acceptGroupJoinRequestAsync :: User -> GroupInfo -> UserContactRequest -> GroupMemberRole -> Maybe IncognitoProfile -> CM GroupMember
|
|
|
|
acceptGroupJoinRequestAsync
|
|
|
|
user
|
|
|
|
gInfo@GroupInfo {groupProfile, membership, businessChat}
|
|
|
|
ucr@UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange}
|
|
|
|
gLinkMemRole
|
|
|
|
incognitoProfile = do
|
|
|
|
gVar <- asks random
|
|
|
|
(groupMemberId, memberId) <- withStore $ \db -> do
|
|
|
|
liftIO $ deleteContactRequestRec db user ucr
|
2025-02-25 14:05:49 +04:00
|
|
|
createJoiningMember db gVar user gInfo ucr gLinkMemRole GSMemAccepted
|
2024-12-20 16:54:24 +04:00
|
|
|
currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo
|
|
|
|
let Profile {displayName} = profileToSendOnAccept user incognitoProfile True
|
|
|
|
GroupMember {memberRole = userRole, memberId = userMemberId} = membership
|
|
|
|
msg =
|
|
|
|
XGrpLinkInv $
|
|
|
|
GroupLinkInvitation
|
|
|
|
{ fromMember = MemberIdRole userMemberId userRole,
|
|
|
|
fromMemberName = displayName,
|
|
|
|
invitedMember = MemberIdRole memberId gLinkMemRole,
|
|
|
|
groupProfile,
|
|
|
|
business = businessChat,
|
|
|
|
groupSize = Just currentMemCount
|
|
|
|
}
|
|
|
|
subMode <- chatReadVar subscriptionMode
|
|
|
|
vr <- chatVersionRange
|
|
|
|
let chatV = vr `peerConnChatVersion` cReqChatVRange
|
|
|
|
connIds <- agentAcceptContactAsync user True invId msg subMode PQSupportOff chatV
|
|
|
|
withStore $ \db -> do
|
2025-02-25 14:05:49 +04:00
|
|
|
liftIO $ createJoiningMemberConnection db user connIds chatV ucr groupMemberId subMode
|
|
|
|
getGroupMemberById db vr user groupMemberId
|
|
|
|
|
|
|
|
acceptGroupJoinSendRejectAsync :: User -> GroupInfo -> UserContactRequest -> GroupRejectionReason -> CM GroupMember
|
|
|
|
acceptGroupJoinSendRejectAsync
|
|
|
|
user
|
|
|
|
gInfo@GroupInfo {groupProfile, membership}
|
|
|
|
ucr@UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange}
|
|
|
|
rejectionReason = do
|
|
|
|
gVar <- asks random
|
|
|
|
(groupMemberId, memberId) <- withStore $ \db -> do
|
|
|
|
liftIO $ deleteContactRequestRec db user ucr
|
|
|
|
createJoiningMember db gVar user gInfo ucr GRObserver GSMemRejected
|
|
|
|
let GroupMember {memberRole = userRole, memberId = userMemberId} = membership
|
|
|
|
msg =
|
|
|
|
XGrpLinkReject $
|
|
|
|
GroupLinkRejection
|
|
|
|
{ fromMember = MemberIdRole userMemberId userRole,
|
|
|
|
invitedMember = MemberIdRole memberId GRObserver,
|
|
|
|
groupProfile,
|
|
|
|
rejectionReason
|
|
|
|
}
|
|
|
|
subMode <- chatReadVar subscriptionMode
|
|
|
|
vr <- chatVersionRange
|
|
|
|
let chatV = vr `peerConnChatVersion` cReqChatVRange
|
|
|
|
connIds <- agentAcceptContactAsync user False invId msg subMode PQSupportOff chatV
|
|
|
|
withStore $ \db -> do
|
|
|
|
liftIO $ createJoiningMemberConnection db user connIds chatV ucr groupMemberId subMode
|
2024-12-20 16:54:24 +04:00
|
|
|
getGroupMemberById db vr user groupMemberId
|
|
|
|
|
|
|
|
acceptBusinessJoinRequestAsync :: User -> UserContactRequest -> CM GroupInfo
|
|
|
|
acceptBusinessJoinRequestAsync
|
|
|
|
user
|
|
|
|
ucr@UserContactRequest {contactRequestId, agentInvitationId = AgentInvId invId, cReqChatVRange} = do
|
|
|
|
vr <- chatVersionRange
|
|
|
|
gVar <- asks random
|
|
|
|
let userProfile@Profile {displayName, preferences} = profileToSendOnAccept user Nothing True
|
|
|
|
groupPreferences = maybe defaultBusinessGroupPrefs businessGroupPrefs preferences
|
|
|
|
(gInfo, clientMember) <- withStore $ \db -> do
|
|
|
|
liftIO $ deleteContactRequest db user contactRequestId
|
|
|
|
createBusinessRequestGroup db vr gVar user ucr groupPreferences
|
|
|
|
let GroupInfo {membership} = gInfo
|
|
|
|
GroupMember {memberRole = userRole, memberId = userMemberId} = membership
|
|
|
|
GroupMember {groupMemberId, memberId} = clientMember
|
|
|
|
msg =
|
|
|
|
XGrpLinkInv $
|
|
|
|
GroupLinkInvitation
|
|
|
|
{ fromMember = MemberIdRole userMemberId userRole,
|
|
|
|
fromMemberName = displayName,
|
|
|
|
invitedMember = MemberIdRole memberId GRMember,
|
|
|
|
groupProfile = businessGroupProfile userProfile groupPreferences,
|
|
|
|
-- This refers to the "title member" that defines the group name and profile.
|
|
|
|
-- This coincides with fromMember to be current user when accepting the connecting user,
|
|
|
|
-- but it will be different when inviting somebody else.
|
|
|
|
business = Just $ BusinessChatInfo {chatType = BCBusiness, businessId = userMemberId, customerId = memberId},
|
|
|
|
groupSize = Just 1
|
|
|
|
}
|
|
|
|
subMode <- chatReadVar subscriptionMode
|
|
|
|
let chatV = vr `peerConnChatVersion` cReqChatVRange
|
|
|
|
connIds <- agentAcceptContactAsync user True invId msg subMode PQSupportOff chatV
|
2025-02-25 14:05:49 +04:00
|
|
|
withStore' $ \db -> createJoiningMemberConnection db user connIds chatV ucr groupMemberId subMode
|
2024-12-20 16:54:24 +04:00
|
|
|
let cd = CDGroupSnd gInfo
|
|
|
|
createInternalChatItem user cd (CISndGroupE2EEInfo E2EInfo {pqEnabled = PQEncOff}) Nothing
|
|
|
|
createGroupFeatureItems user cd CISndGroupFeature gInfo
|
|
|
|
pure gInfo
|
|
|
|
where
|
|
|
|
businessGroupProfile :: Profile -> GroupPreferences -> GroupProfile
|
|
|
|
businessGroupProfile Profile {displayName, fullName, image} groupPreferences =
|
|
|
|
GroupProfile {displayName, fullName, description = Nothing, image, groupPreferences = Just groupPreferences}
|
|
|
|
|
|
|
|
profileToSendOnAccept :: User -> Maybe IncognitoProfile -> Bool -> Profile
|
|
|
|
profileToSendOnAccept user ip = userProfileToSend user (getIncognitoProfile <$> ip) Nothing
|
|
|
|
where
|
|
|
|
getIncognitoProfile = \case
|
|
|
|
NewIncognito p -> p
|
|
|
|
ExistingIncognito lp -> fromLocalProfile lp
|
|
|
|
|
|
|
|
deleteGroupLink' :: User -> GroupInfo -> CM ()
|
|
|
|
deleteGroupLink' user gInfo = do
|
|
|
|
vr <- chatVersionRange
|
|
|
|
conn <- withStore $ \db -> getGroupLinkConnection db vr user gInfo
|
|
|
|
deleteGroupLink_ user gInfo conn
|
|
|
|
|
|
|
|
deleteGroupLinkIfExists :: User -> GroupInfo -> CM ()
|
|
|
|
deleteGroupLinkIfExists user gInfo = do
|
|
|
|
vr <- chatVersionRange
|
|
|
|
conn_ <- eitherToMaybe <$> withStore' (\db -> runExceptT $ getGroupLinkConnection db vr user gInfo)
|
|
|
|
mapM_ (deleteGroupLink_ user gInfo) conn_
|
|
|
|
|
|
|
|
deleteGroupLink_ :: User -> GroupInfo -> Connection -> CM ()
|
|
|
|
deleteGroupLink_ user gInfo conn = do
|
|
|
|
deleteAgentConnectionAsync user $ aConnId conn
|
|
|
|
withStore' $ \db -> deleteGroupLink db user gInfo
|
|
|
|
|
|
|
|
startProximateTimedItemThread :: User -> (ChatRef, ChatItemId) -> UTCTime -> CM ()
|
|
|
|
startProximateTimedItemThread user itemRef deleteAt = do
|
|
|
|
interval <- asks (cleanupManagerInterval . config)
|
|
|
|
ts <- liftIO getCurrentTime
|
|
|
|
when (diffUTCTime deleteAt ts <= interval) $
|
|
|
|
startTimedItemThread user itemRef deleteAt
|
|
|
|
|
|
|
|
startTimedItemThread :: User -> (ChatRef, ChatItemId) -> UTCTime -> CM ()
|
|
|
|
startTimedItemThread user itemRef deleteAt = do
|
|
|
|
itemThreads <- asks timedItemThreads
|
|
|
|
threadTVar_ <- atomically $ do
|
|
|
|
exists <- TM.member itemRef itemThreads
|
|
|
|
if not exists
|
|
|
|
then do
|
|
|
|
threadTVar <- newTVar Nothing
|
|
|
|
TM.insert itemRef threadTVar itemThreads
|
|
|
|
pure $ Just threadTVar
|
|
|
|
else pure Nothing
|
|
|
|
forM_ threadTVar_ $ \threadTVar -> do
|
|
|
|
tId <- mkWeakThreadId =<< deleteTimedItem user itemRef deleteAt `forkFinally` const (atomically $ TM.delete itemRef itemThreads)
|
|
|
|
atomically $ writeTVar threadTVar (Just tId)
|
|
|
|
|
|
|
|
deleteTimedItem :: User -> (ChatRef, ChatItemId) -> UTCTime -> CM ()
|
|
|
|
deleteTimedItem user (ChatRef cType chatId, itemId) deleteAt = do
|
|
|
|
ts <- liftIO getCurrentTime
|
|
|
|
liftIO $ threadDelay' $ diffToMicroseconds $ diffUTCTime deleteAt ts
|
|
|
|
lift waitChatStartedAndActivated
|
|
|
|
vr <- chatVersionRange
|
|
|
|
case cType of
|
|
|
|
CTDirect -> do
|
|
|
|
(ct, ci) <- withStore $ \db -> (,) <$> getContact db vr user chatId <*> getDirectChatItem db user chatId itemId
|
|
|
|
deleteDirectCIs user ct [ci] True True >>= toView
|
|
|
|
CTGroup -> do
|
|
|
|
(gInfo, ci) <- withStore $ \db -> (,) <$> getGroupInfo db vr user chatId <*> getGroupChatItem db user chatId itemId
|
|
|
|
deletedTs <- liftIO getCurrentTime
|
|
|
|
deleteGroupCIs user gInfo [ci] True True Nothing deletedTs >>= toView
|
|
|
|
_ -> toView . CRChatError (Just user) . ChatError $ CEInternalError "bad deleteTimedItem cType"
|
|
|
|
|
|
|
|
startUpdatedTimedItemThread :: User -> ChatRef -> ChatItem c d -> ChatItem c d -> CM ()
|
|
|
|
startUpdatedTimedItemThread user chatRef ci ci' =
|
|
|
|
case (chatItemTimed ci >>= timedDeleteAt', chatItemTimed ci' >>= timedDeleteAt') of
|
|
|
|
(Nothing, Just deleteAt') ->
|
|
|
|
startProximateTimedItemThread user (chatRef, chatItemId' ci') deleteAt'
|
|
|
|
_ -> pure ()
|
|
|
|
|
|
|
|
metaBrokerTs :: MsgMeta -> UTCTime
|
|
|
|
metaBrokerTs MsgMeta {broker = (_, brokerTs)} = brokerTs
|
|
|
|
|
|
|
|
createContactPQSndItem :: User -> Contact -> Connection -> PQEncryption -> CM (Contact, Connection)
|
|
|
|
createContactPQSndItem user ct conn@Connection {pqSndEnabled} pqSndEnabled' =
|
|
|
|
flip catchChatError (const $ pure (ct, conn)) $ case (pqSndEnabled, pqSndEnabled') of
|
|
|
|
(Just b, b') | b' /= b -> createPQItem $ CISndConnEvent (SCEPqEnabled pqSndEnabled')
|
|
|
|
(Nothing, PQEncOn) -> createPQItem $ CISndDirectE2EEInfo (E2EInfo pqSndEnabled')
|
|
|
|
_ -> pure (ct, conn)
|
|
|
|
where
|
|
|
|
createPQItem ciContent = do
|
|
|
|
let conn' = conn {pqSndEnabled = Just pqSndEnabled'} :: Connection
|
|
|
|
ct' = ct {activeConn = Just conn'} :: Contact
|
|
|
|
when (contactPQEnabled ct /= contactPQEnabled ct') $ do
|
|
|
|
createInternalChatItem user (CDDirectSnd ct') ciContent Nothing
|
|
|
|
toView $ CRContactPQEnabled user ct' pqSndEnabled'
|
|
|
|
pure (ct', conn')
|
|
|
|
|
|
|
|
updateContactPQRcv :: User -> Contact -> Connection -> PQEncryption -> CM (Contact, Connection)
|
|
|
|
updateContactPQRcv user ct conn@Connection {connId, pqRcvEnabled} pqRcvEnabled' =
|
|
|
|
flip catchChatError (const $ pure (ct, conn)) $ case (pqRcvEnabled, pqRcvEnabled') of
|
|
|
|
(Just b, b') | b' /= b -> updatePQ $ CIRcvConnEvent (RCEPqEnabled pqRcvEnabled')
|
|
|
|
(Nothing, PQEncOn) -> updatePQ $ CIRcvDirectE2EEInfo (E2EInfo pqRcvEnabled')
|
|
|
|
_ -> pure (ct, conn)
|
|
|
|
where
|
|
|
|
updatePQ ciContent = do
|
|
|
|
withStore' $ \db -> updateConnPQRcvEnabled db connId pqRcvEnabled'
|
|
|
|
let conn' = conn {pqRcvEnabled = Just pqRcvEnabled'} :: Connection
|
|
|
|
ct' = ct {activeConn = Just conn'} :: Contact
|
|
|
|
when (contactPQEnabled ct /= contactPQEnabled ct') $ do
|
|
|
|
createInternalChatItem user (CDDirectRcv ct') ciContent Nothing
|
|
|
|
toView $ CRContactPQEnabled user ct' pqRcvEnabled'
|
|
|
|
pure (ct', conn')
|
|
|
|
|
|
|
|
updatePeerChatVRange :: Connection -> VersionRangeChat -> CM Connection
|
|
|
|
updatePeerChatVRange conn@Connection {connId, connChatVersion = v, peerChatVRange, connType, pqSupport, pqEncryption} msgVRange = do
|
|
|
|
v' <- lift $ upgradedConnVersion v msgVRange
|
|
|
|
conn' <-
|
|
|
|
if msgVRange /= peerChatVRange || v' /= v
|
|
|
|
then do
|
|
|
|
withStore' $ \db -> setPeerChatVRange db connId v' msgVRange
|
|
|
|
pure conn {connChatVersion = v', peerChatVRange = msgVRange}
|
|
|
|
else pure conn
|
|
|
|
-- TODO v6.0 remove/review: for contacts only version upgrade should trigger enabling PQ support/encryption
|
|
|
|
if connType == ConnContact && v' >= pqEncryptionCompressionVersion && (pqSupport /= PQSupportOn || pqEncryption /= PQEncOn)
|
|
|
|
then do
|
|
|
|
withStore' $ \db -> updateConnSupportPQ db connId PQSupportOn PQEncOn
|
|
|
|
pure conn' {pqSupport = PQSupportOn, pqEncryption = PQEncOn}
|
|
|
|
else pure conn'
|
|
|
|
|
|
|
|
updateMemberChatVRange :: GroupMember -> Connection -> VersionRangeChat -> CM (GroupMember, Connection)
|
|
|
|
updateMemberChatVRange mem@GroupMember {groupMemberId} conn@Connection {connId, connChatVersion = v, peerChatVRange} msgVRange = do
|
|
|
|
v' <- lift $ upgradedConnVersion v msgVRange
|
|
|
|
if msgVRange /= peerChatVRange || v' /= v
|
|
|
|
then do
|
|
|
|
withStore' $ \db -> do
|
|
|
|
setPeerChatVRange db connId v' msgVRange
|
|
|
|
setMemberChatVRange db groupMemberId msgVRange
|
|
|
|
let conn' = conn {connChatVersion = v', peerChatVRange = msgVRange}
|
|
|
|
pure (mem {memberChatVRange = msgVRange, activeConn = Just conn'}, conn')
|
|
|
|
else pure (mem, conn)
|
|
|
|
|
|
|
|
upgradedConnVersion :: VersionChat -> VersionRangeChat -> CM' VersionChat
|
|
|
|
upgradedConnVersion v peerVR = do
|
|
|
|
vr <- chatVersionRange'
|
|
|
|
-- don't allow reducing agreed connection version
|
|
|
|
pure $ maybe v (\(Compatible v') -> max v v') $ vr `compatibleVersion` peerVR
|
|
|
|
|
|
|
|
parseFileDescription :: FilePartyI p => Text -> CM (ValidFileDescription p)
|
|
|
|
parseFileDescription =
|
|
|
|
liftEither . first (ChatError . CEInvalidFileDescription) . (strDecode . encodeUtf8)
|
|
|
|
|
|
|
|
sendDirectFileInline :: User -> Contact -> FileTransferMeta -> SharedMsgId -> CM ()
|
|
|
|
sendDirectFileInline user ct ft sharedMsgId = do
|
|
|
|
msgDeliveryId <- sendFileInline_ ft sharedMsgId $ sendDirectContactMessage user ct
|
|
|
|
withStore $ \db -> updateSndDirectFTDelivery db ct ft msgDeliveryId
|
|
|
|
|
|
|
|
sendMemberFileInline :: GroupMember -> Connection -> FileTransferMeta -> SharedMsgId -> CM ()
|
|
|
|
sendMemberFileInline m@GroupMember {groupId} conn ft sharedMsgId = do
|
|
|
|
msgDeliveryId <- sendFileInline_ ft sharedMsgId $ \msg -> do
|
|
|
|
(sndMsg, msgDeliveryId, _) <- sendDirectMemberMessage conn msg groupId
|
|
|
|
pure (sndMsg, msgDeliveryId)
|
|
|
|
withStore' $ \db -> updateSndGroupFTDelivery db m conn ft msgDeliveryId
|
|
|
|
|
|
|
|
sendFileInline_ :: FileTransferMeta -> SharedMsgId -> (ChatMsgEvent 'Binary -> CM (SndMessage, Int64)) -> CM Int64
|
|
|
|
sendFileInline_ FileTransferMeta {filePath, chunkSize} sharedMsgId sendMsg =
|
|
|
|
sendChunks 1 =<< liftIO . B.readFile =<< lift (toFSFilePath filePath)
|
|
|
|
where
|
|
|
|
sendChunks chunkNo bytes = do
|
|
|
|
let (chunk, rest) = B.splitAt chSize bytes
|
|
|
|
(_, msgDeliveryId) <- sendMsg $ BFileChunk sharedMsgId $ FileChunk chunkNo chunk
|
|
|
|
if B.null rest
|
|
|
|
then pure msgDeliveryId
|
|
|
|
else sendChunks (chunkNo + 1) rest
|
|
|
|
chSize = fromIntegral chunkSize
|
|
|
|
|
|
|
|
parseChatMessage :: Connection -> ByteString -> CM (ChatMessage 'Json)
|
|
|
|
parseChatMessage conn s = do
|
|
|
|
case parseChatMessages s of
|
|
|
|
[msg] -> liftEither . first (ChatError . errType) $ (\(ACMsg _ m) -> checkEncoding m) =<< msg
|
|
|
|
_ -> throwChatError $ CEException "parseChatMessage: single message is expected"
|
|
|
|
where
|
|
|
|
errType = CEInvalidChatMessage conn Nothing (safeDecodeUtf8 s)
|
|
|
|
{-# INLINE parseChatMessage #-}
|
|
|
|
|
|
|
|
sendFileChunk :: User -> SndFileTransfer -> CM ()
|
|
|
|
sendFileChunk user ft@SndFileTransfer {fileId, fileStatus, agentConnId = AgentConnId acId} =
|
|
|
|
unless (fileStatus == FSComplete || fileStatus == FSCancelled) $ do
|
|
|
|
vr <- chatVersionRange
|
|
|
|
withStore' (`createSndFileChunk` ft) >>= \case
|
|
|
|
Just chunkNo -> sendFileChunkNo ft chunkNo
|
|
|
|
Nothing -> do
|
|
|
|
ci <- withStore $ \db -> do
|
|
|
|
liftIO $ updateSndFileStatus db ft FSComplete
|
|
|
|
liftIO $ deleteSndFileChunks db ft
|
|
|
|
updateDirectCIFileStatus db vr user fileId CIFSSndComplete
|
|
|
|
toView $ CRSndFileComplete user ci ft
|
|
|
|
lift $ closeFileHandle fileId sndFiles
|
|
|
|
deleteAgentConnectionAsync user acId
|
|
|
|
|
|
|
|
sendFileChunkNo :: SndFileTransfer -> Integer -> CM ()
|
|
|
|
sendFileChunkNo ft@SndFileTransfer {agentConnId = AgentConnId acId} chunkNo = do
|
|
|
|
chunkBytes <- readFileChunk ft chunkNo
|
|
|
|
(msgId, _) <- withAgent $ \a -> sendMessage a acId PQEncOff SMP.noMsgFlags $ smpEncode FileChunk {chunkNo, chunkBytes}
|
|
|
|
withStore' $ \db -> updateSndFileChunkMsg db ft chunkNo msgId
|
|
|
|
|
|
|
|
readFileChunk :: SndFileTransfer -> Integer -> CM ByteString
|
|
|
|
readFileChunk SndFileTransfer {fileId, filePath, chunkSize} chunkNo = do
|
|
|
|
fsFilePath <- lift $ toFSFilePath filePath
|
|
|
|
read_ fsFilePath `catchThrow` (ChatError . CEFileRead filePath . show)
|
|
|
|
where
|
|
|
|
read_ fsFilePath = do
|
|
|
|
h <- getFileHandle fileId fsFilePath sndFiles ReadMode
|
|
|
|
pos <- hTell h
|
|
|
|
let pos' = (chunkNo - 1) * chunkSize
|
|
|
|
when (pos /= pos') $ hSeek h AbsoluteSeek pos'
|
|
|
|
liftIO . B.hGet h $ fromInteger chunkSize
|
|
|
|
|
|
|
|
parseFileChunk :: ByteString -> CM FileChunk
|
|
|
|
parseFileChunk = liftEither . first (ChatError . CEFileRcvChunk) . smpDecode
|
|
|
|
|
|
|
|
appendFileChunk :: RcvFileTransfer -> Integer -> ByteString -> Bool -> CM ()
|
|
|
|
appendFileChunk ft@RcvFileTransfer {fileId, fileStatus, cryptoArgs, fileInvitation = FileInvitation {fileName}} chunkNo chunk final =
|
|
|
|
case fileStatus of
|
|
|
|
RFSConnected RcvFileInfo {filePath} -> append_ filePath
|
|
|
|
-- sometimes update of file transfer status to FSConnected
|
|
|
|
-- doesn't complete in time before MSG with first file chunk
|
|
|
|
RFSAccepted RcvFileInfo {filePath} -> append_ filePath
|
|
|
|
RFSCancelled _ -> pure ()
|
|
|
|
_ -> throwChatError $ CEFileInternal "receiving file transfer not in progress"
|
|
|
|
where
|
|
|
|
append_ :: FilePath -> CM ()
|
|
|
|
append_ filePath = do
|
|
|
|
fsFilePath <- lift $ toFSFilePath filePath
|
|
|
|
h <- getFileHandle fileId fsFilePath rcvFiles AppendMode
|
|
|
|
liftIO (B.hPut h chunk >> hFlush h) `catchThrow` (fileErr . show)
|
|
|
|
withStore' $ \db -> updatedRcvFileChunkStored db ft chunkNo
|
|
|
|
when final $ do
|
|
|
|
lift $ closeFileHandle fileId rcvFiles
|
|
|
|
forM_ cryptoArgs $ \cfArgs -> do
|
|
|
|
tmpFile <- lift getChatTempDirectory >>= liftIO . (`uniqueCombine` fileName)
|
|
|
|
tryChatError (liftError encryptErr $ encryptFile fsFilePath tmpFile cfArgs) >>= \case
|
|
|
|
Right () -> do
|
|
|
|
removeFile fsFilePath `catchChatError` \_ -> pure ()
|
|
|
|
renameFile tmpFile fsFilePath
|
|
|
|
Left e -> do
|
|
|
|
toView $ CRChatError Nothing e
|
|
|
|
removeFile tmpFile `catchChatError` \_ -> pure ()
|
|
|
|
withStore' (`removeFileCryptoArgs` fileId)
|
|
|
|
where
|
|
|
|
encryptErr e = fileErr $ e <> ", received file not encrypted"
|
|
|
|
fileErr = ChatError . CEFileWrite filePath
|
|
|
|
|
|
|
|
getFileHandle :: Int64 -> FilePath -> (ChatController -> TVar (Map Int64 Handle)) -> IOMode -> CM Handle
|
|
|
|
getFileHandle fileId filePath files ioMode = do
|
|
|
|
fs <- asks files
|
|
|
|
h_ <- M.lookup fileId <$> readTVarIO fs
|
|
|
|
maybe (newHandle fs) pure h_
|
|
|
|
where
|
|
|
|
newHandle fs = do
|
|
|
|
h <- openFile filePath ioMode `catchThrow` (ChatError . CEFileInternal . show)
|
|
|
|
atomically . modifyTVar fs $ M.insert fileId h
|
|
|
|
pure h
|
|
|
|
|
|
|
|
isFileActive :: Int64 -> (ChatController -> TVar (Map Int64 Handle)) -> CM Bool
|
|
|
|
isFileActive fileId files = do
|
|
|
|
fs <- asks files
|
|
|
|
isJust . M.lookup fileId <$> readTVarIO fs
|
|
|
|
|
|
|
|
cancelRcvFileTransfer :: User -> RcvFileTransfer -> CM (Maybe ConnId)
|
|
|
|
cancelRcvFileTransfer user ft@RcvFileTransfer {fileId, xftpRcvFile, rcvFileInline} =
|
|
|
|
cancel' `catchChatError` (\e -> toView (CRChatError (Just user) e) $> fileConnId)
|
|
|
|
where
|
|
|
|
cancel' = do
|
|
|
|
lift $ closeFileHandle fileId rcvFiles
|
|
|
|
withStore' $ \db -> do
|
|
|
|
updateFileCancelled db user fileId CIFSRcvCancelled
|
|
|
|
updateRcvFileStatus db fileId FSCancelled
|
|
|
|
deleteRcvFileChunks db ft
|
|
|
|
case xftpRcvFile of
|
|
|
|
Just XFTPRcvFile {agentRcvFileId = Just (AgentRcvFileId aFileId), agentRcvFileDeleted} ->
|
|
|
|
unless agentRcvFileDeleted $ agentXFTPDeleteRcvFile aFileId fileId
|
|
|
|
_ -> pure ()
|
|
|
|
pure fileConnId
|
|
|
|
fileConnId = if isNothing xftpRcvFile && isNothing rcvFileInline then liveRcvFileTransferConnId ft else Nothing
|
|
|
|
|
|
|
|
cancelSndFile :: User -> FileTransferMeta -> [SndFileTransfer] -> Bool -> CM [ConnId]
|
|
|
|
cancelSndFile user FileTransferMeta {fileId, xftpSndFile} fts sendCancel = do
|
|
|
|
withStore' (\db -> updateFileCancelled db user fileId CIFSSndCancelled)
|
|
|
|
`catchChatError` (toView . CRChatError (Just user))
|
|
|
|
case xftpSndFile of
|
|
|
|
Nothing ->
|
|
|
|
catMaybes <$> forM fts (\ft -> cancelSndFileTransfer user ft sendCancel)
|
|
|
|
Just xsf -> do
|
|
|
|
forM_ fts (\ft -> cancelSndFileTransfer user ft False)
|
|
|
|
lift (agentXFTPDeleteSndFileRemote user xsf fileId) `catchChatError` (toView . CRChatError (Just user))
|
|
|
|
pure []
|
|
|
|
|
|
|
|
-- TODO v6.0 remove
|
|
|
|
cancelSndFileTransfer :: User -> SndFileTransfer -> Bool -> CM (Maybe ConnId)
|
|
|
|
cancelSndFileTransfer user@User {userId} ft@SndFileTransfer {fileId, connId, agentConnId = AgentConnId acId, fileStatus, fileInline} sendCancel =
|
|
|
|
if fileStatus == FSCancelled || fileStatus == FSComplete
|
|
|
|
then pure Nothing
|
|
|
|
else cancel' `catchChatError` (\e -> toView (CRChatError (Just user) e) $> fileConnId)
|
|
|
|
where
|
|
|
|
cancel' = do
|
|
|
|
withStore' $ \db -> do
|
|
|
|
updateSndFileStatus db ft FSCancelled
|
|
|
|
deleteSndFileChunks db ft
|
|
|
|
when sendCancel $ case fileInline of
|
|
|
|
Just _ -> do
|
|
|
|
vr <- chatVersionRange
|
|
|
|
(sharedMsgId, conn) <- withStore $ \db -> (,) <$> getSharedMsgIdByFileId db userId fileId <*> getConnectionById db vr user connId
|
|
|
|
void $ sendDirectMessage_ conn (BFileChunk sharedMsgId FileChunkCancel) (ConnectionId connId)
|
|
|
|
_ -> withAgent $ \a -> void . sendMessage a acId PQEncOff SMP.noMsgFlags $ smpEncode FileChunkCancel
|
|
|
|
pure fileConnId
|
|
|
|
fileConnId = if isNothing fileInline then Just acId else Nothing
|
|
|
|
|
|
|
|
closeFileHandle :: Int64 -> (ChatController -> TVar (Map Int64 Handle)) -> CM' ()
|
|
|
|
closeFileHandle fileId files = do
|
|
|
|
fs <- asks files
|
|
|
|
h_ <- atomically . stateTVar fs $ \m -> (M.lookup fileId m, M.delete fileId m)
|
|
|
|
liftIO $ mapM_ hClose h_ `catchAll_` pure ()
|
|
|
|
|
|
|
|
deleteMembersConnections :: User -> [GroupMember] -> CM ()
|
|
|
|
deleteMembersConnections user members = deleteMembersConnections' user members False
|
|
|
|
|
|
|
|
deleteMembersConnections' :: User -> [GroupMember] -> Bool -> CM ()
|
|
|
|
deleteMembersConnections' user members waitDelivery = do
|
2025-01-31 18:47:59 +04:00
|
|
|
let memberConns = mapMaybe (\GroupMember {activeConn} -> activeConn) members
|
2024-12-20 16:54:24 +04:00
|
|
|
deleteAgentConnectionsAsync' user (map aConnId memberConns) waitDelivery
|
2025-01-31 18:47:59 +04:00
|
|
|
lift . void . withStoreBatch' $ \db -> map (\Connection {connId} -> deleteConnectionRecord db user connId) memberConns
|
2024-12-20 16:54:24 +04:00
|
|
|
|
|
|
|
deleteMemberConnection :: User -> GroupMember -> CM ()
|
|
|
|
deleteMemberConnection user mem = deleteMemberConnection' user mem False
|
|
|
|
|
|
|
|
deleteMemberConnection' :: User -> GroupMember -> Bool -> CM ()
|
|
|
|
deleteMemberConnection' user GroupMember {activeConn} waitDelivery = do
|
|
|
|
forM_ activeConn $ \conn -> do
|
|
|
|
deleteAgentConnectionAsync' user (aConnId conn) waitDelivery
|
|
|
|
withStore' $ \db -> updateConnectionStatus db conn ConnDeleted
|
|
|
|
|
|
|
|
deleteOrUpdateMemberRecord :: User -> GroupMember -> CM ()
|
|
|
|
deleteOrUpdateMemberRecord user@User {userId} member =
|
|
|
|
withStore' $ \db ->
|
|
|
|
checkGroupMemberHasItems db user member >>= \case
|
|
|
|
Just _ -> updateGroupMemberStatus db userId member GSMemRemoved
|
|
|
|
Nothing -> deleteGroupMember db user member
|
|
|
|
|
|
|
|
sendDirectContactMessages :: MsgEncodingI e => User -> Contact -> NonEmpty (ChatMsgEvent e) -> CM [Either ChatError SndMessage]
|
|
|
|
sendDirectContactMessages user ct events = do
|
|
|
|
Connection {connChatVersion = v} <- liftEither $ contactSendConn_ ct
|
|
|
|
if v >= batchSend2Version
|
|
|
|
then sendDirectContactMessages' user ct events
|
|
|
|
else forM (L.toList events) $ \evt ->
|
|
|
|
(Right . fst <$> sendDirectContactMessage user ct evt) `catchChatError` \e -> pure (Left e)
|
|
|
|
|
|
|
|
sendDirectContactMessages' :: MsgEncodingI e => User -> Contact -> NonEmpty (ChatMsgEvent e) -> CM [Either ChatError SndMessage]
|
|
|
|
sendDirectContactMessages' user ct events = do
|
|
|
|
conn@Connection {connId} <- liftEither $ contactSendConn_ ct
|
|
|
|
let idsEvts = L.map (ConnectionId connId,) events
|
|
|
|
msgFlags = MsgFlags {notification = any (hasNotification . toCMEventTag) events}
|
|
|
|
sndMsgs_ <- lift $ createSndMessages idsEvts
|
|
|
|
(sndMsgs', pqEnc_) <- batchSendConnMessagesB user conn msgFlags sndMsgs_
|
|
|
|
forM_ pqEnc_ $ \pqEnc' -> void $ createContactPQSndItem user ct conn pqEnc'
|
|
|
|
pure sndMsgs'
|
|
|
|
|
|
|
|
sendDirectContactMessage :: MsgEncodingI e => User -> Contact -> ChatMsgEvent e -> CM (SndMessage, Int64)
|
|
|
|
sendDirectContactMessage user ct chatMsgEvent = do
|
|
|
|
conn@Connection {connId} <- liftEither $ contactSendConn_ ct
|
|
|
|
r <- sendDirectMessage_ conn chatMsgEvent (ConnectionId connId)
|
|
|
|
let (sndMessage, msgDeliveryId, pqEnc') = r
|
|
|
|
void $ createContactPQSndItem user ct conn pqEnc'
|
|
|
|
pure (sndMessage, msgDeliveryId)
|
|
|
|
|
|
|
|
contactSendConn_ :: Contact -> Either ChatError Connection
|
|
|
|
contactSendConn_ ct@Contact {activeConn} = case activeConn of
|
|
|
|
Nothing -> err $ CEContactNotReady ct
|
|
|
|
Just conn
|
|
|
|
| not (connReady conn) -> err $ CEContactNotReady ct
|
|
|
|
| not (contactActive ct) -> err $ CEContactNotActive ct
|
|
|
|
| connDisabled conn -> err $ CEContactDisabled ct
|
|
|
|
| otherwise -> Right conn
|
|
|
|
where
|
|
|
|
err = Left . ChatError
|
|
|
|
|
|
|
|
-- unlike sendGroupMemberMessage, this function will not store message as pending
|
|
|
|
-- TODO v5.8 we could remove pending messages once all clients support forwarding
|
|
|
|
sendDirectMemberMessage :: MsgEncodingI e => Connection -> ChatMsgEvent e -> GroupId -> CM (SndMessage, Int64, PQEncryption)
|
|
|
|
sendDirectMemberMessage conn chatMsgEvent groupId = sendDirectMessage_ conn chatMsgEvent (GroupId groupId)
|
|
|
|
|
|
|
|
sendDirectMessage_ :: MsgEncodingI e => Connection -> ChatMsgEvent e -> ConnOrGroupId -> CM (SndMessage, Int64, PQEncryption)
|
|
|
|
sendDirectMessage_ conn chatMsgEvent connOrGroupId = do
|
|
|
|
when (connDisabled conn) $ throwChatError (CEConnectionDisabled conn)
|
|
|
|
msg@SndMessage {msgId, msgBody} <- createSndMessage chatMsgEvent connOrGroupId
|
|
|
|
-- TODO move compressed body to SndMessage and compress in createSndMessage
|
|
|
|
(msgDeliveryId, pqEnc') <- deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId
|
|
|
|
pure (msg, msgDeliveryId, pqEnc')
|
|
|
|
|
|
|
|
createSndMessage :: MsgEncodingI e => ChatMsgEvent e -> ConnOrGroupId -> CM SndMessage
|
|
|
|
createSndMessage chatMsgEvent connOrGroupId =
|
|
|
|
liftEither . runIdentity =<< lift (createSndMessages $ Identity (connOrGroupId, chatMsgEvent))
|
|
|
|
|
|
|
|
createSndMessages :: forall e t. (MsgEncodingI e, Traversable t) => t (ConnOrGroupId, ChatMsgEvent e) -> CM' (t (Either ChatError SndMessage))
|
|
|
|
createSndMessages idsEvents = do
|
|
|
|
g <- asks random
|
|
|
|
vr <- chatVersionRange'
|
|
|
|
withStoreBatch $ \db -> fmap (createMsg db g vr) idsEvents
|
|
|
|
where
|
|
|
|
createMsg :: DB.Connection -> TVar ChaChaDRG -> VersionRangeChat -> (ConnOrGroupId, ChatMsgEvent e) -> IO (Either ChatError SndMessage)
|
|
|
|
createMsg db g vr (connOrGroupId, evnt) = runExceptT $ do
|
|
|
|
withExceptT ChatErrorStore $ createNewSndMessage db g connOrGroupId evnt encodeMessage
|
|
|
|
where
|
|
|
|
encodeMessage sharedMsgId =
|
|
|
|
encodeChatMessage maxEncodedMsgLength ChatMessage {chatVRange = vr, msgId = Just sharedMsgId, chatMsgEvent = evnt}
|
|
|
|
|
|
|
|
sendGroupMemberMessages :: forall e. MsgEncodingI e => User -> Connection -> NonEmpty (ChatMsgEvent e) -> GroupId -> CM ()
|
|
|
|
sendGroupMemberMessages user conn events groupId = do
|
|
|
|
when (connDisabled conn) $ throwChatError (CEConnectionDisabled conn)
|
|
|
|
let idsEvts = L.map (GroupId groupId,) events
|
|
|
|
(errs, msgs) <- lift $ partitionEithers . L.toList <$> createSndMessages idsEvts
|
|
|
|
unless (null errs) $ toView $ CRChatErrors (Just user) errs
|
|
|
|
forM_ (L.nonEmpty msgs) $ \msgs' ->
|
|
|
|
batchSendConnMessages user conn MsgFlags {notification = True} msgs'
|
|
|
|
|
|
|
|
batchSendConnMessages :: User -> Connection -> MsgFlags -> NonEmpty SndMessage -> CM ([Either ChatError SndMessage], Maybe PQEncryption)
|
|
|
|
batchSendConnMessages user conn msgFlags msgs =
|
|
|
|
batchSendConnMessagesB user conn msgFlags $ L.map Right msgs
|
|
|
|
|
|
|
|
batchSendConnMessagesB :: User -> Connection -> MsgFlags -> NonEmpty (Either ChatError SndMessage) -> CM ([Either ChatError SndMessage], Maybe PQEncryption)
|
|
|
|
batchSendConnMessagesB _user conn msgFlags msgs_ = do
|
|
|
|
let batched_ = batchSndMessagesJSON msgs_
|
|
|
|
case L.nonEmpty batched_ of
|
|
|
|
Just batched' -> do
|
2025-02-15 00:12:32 +04:00
|
|
|
let msgReqs = L.map (fmap msgBatchReq_) batched'
|
2024-12-20 16:54:24 +04:00
|
|
|
delivered <- deliverMessagesB msgReqs
|
|
|
|
let msgs' = concat $ L.zipWith flattenMsgs batched' delivered
|
|
|
|
pqEnc = findLastPQEnc delivered
|
|
|
|
when (length msgs' /= length msgs_) $ logError "batchSendConnMessagesB: msgs_ and msgs' length mismatch"
|
|
|
|
pure (msgs', pqEnc)
|
|
|
|
Nothing -> pure ([], Nothing)
|
|
|
|
where
|
2025-02-15 00:12:32 +04:00
|
|
|
msgBatchReq_ :: MsgBatch -> ChatMsgReq
|
|
|
|
msgBatchReq_ (MsgBatch batchBody sndMsgs) =
|
|
|
|
(conn, msgFlags, (vrValue batchBody, map (\SndMessage {msgId} -> msgId) sndMsgs))
|
2024-12-20 16:54:24 +04:00
|
|
|
flattenMsgs :: Either ChatError MsgBatch -> Either ChatError ([Int64], PQEncryption) -> [Either ChatError SndMessage]
|
|
|
|
flattenMsgs (Right (MsgBatch _ sndMsgs)) (Right _) = map Right sndMsgs
|
|
|
|
flattenMsgs (Right (MsgBatch _ sndMsgs)) (Left ce) = replicate (length sndMsgs) (Left ce)
|
|
|
|
flattenMsgs (Left ce) _ = [Left ce] -- restore original ChatError
|
|
|
|
findLastPQEnc :: NonEmpty (Either ChatError ([Int64], PQEncryption)) -> Maybe PQEncryption
|
|
|
|
findLastPQEnc = foldr' (\x acc -> case x of Right (_, pqEnc) -> Just pqEnc; Left _ -> acc) Nothing
|
|
|
|
|
|
|
|
batchSndMessagesJSON :: NonEmpty (Either ChatError SndMessage) -> [Either ChatError MsgBatch]
|
|
|
|
batchSndMessagesJSON = batchMessages maxEncodedMsgLength . L.toList
|
|
|
|
|
|
|
|
encodeConnInfo :: MsgEncodingI e => ChatMsgEvent e -> CM ByteString
|
|
|
|
encodeConnInfo chatMsgEvent = do
|
|
|
|
vr <- chatVersionRange
|
|
|
|
encodeConnInfoPQ PQSupportOff (maxVersion vr) chatMsgEvent
|
|
|
|
|
|
|
|
encodeConnInfoPQ :: MsgEncodingI e => PQSupport -> VersionChat -> ChatMsgEvent e -> CM ByteString
|
|
|
|
encodeConnInfoPQ pqSup v chatMsgEvent = do
|
|
|
|
vr <- chatVersionRange
|
|
|
|
let info = ChatMessage {chatVRange = vr, msgId = Nothing, chatMsgEvent}
|
|
|
|
case encodeChatMessage maxEncodedInfoLength info of
|
|
|
|
ECMEncoded connInfo -> case pqSup of
|
|
|
|
PQSupportOn | v >= pqEncryptionCompressionVersion && B.length connInfo > maxCompressedInfoLength -> do
|
|
|
|
let connInfo' = compressedBatchMsgBody_ connInfo
|
|
|
|
when (B.length connInfo' > maxCompressedInfoLength) $ throwChatError $ CEException "large compressed info"
|
|
|
|
pure connInfo'
|
|
|
|
_ -> pure connInfo
|
|
|
|
ECMLarge -> throwChatError $ CEException "large info"
|
|
|
|
|
|
|
|
deliverMessage :: Connection -> CMEventTag e -> MsgBody -> MessageId -> CM (Int64, PQEncryption)
|
|
|
|
deliverMessage conn cmEventTag msgBody msgId = do
|
|
|
|
let msgFlags = MsgFlags {notification = hasNotification cmEventTag}
|
|
|
|
deliverMessage' conn msgFlags msgBody msgId
|
|
|
|
|
|
|
|
deliverMessage' :: Connection -> MsgFlags -> MsgBody -> MessageId -> CM (Int64, PQEncryption)
|
|
|
|
deliverMessage' conn msgFlags msgBody msgId =
|
2025-02-15 00:12:32 +04:00
|
|
|
deliverMessages ((conn, msgFlags, (vrValue msgBody, [msgId])) :| []) >>= \case
|
2024-12-20 16:54:24 +04:00
|
|
|
r :| [] -> case r of
|
|
|
|
Right ([deliveryId], pqEnc) -> pure (deliveryId, pqEnc)
|
|
|
|
Right (deliveryIds, _) -> throwChatError $ CEInternalError $ "deliverMessage: expected 1 delivery id, got " <> show (length deliveryIds)
|
|
|
|
Left e -> throwError e
|
|
|
|
rs -> throwChatError $ CEInternalError $ "deliverMessage: expected 1 result, got " <> show (length rs)
|
|
|
|
|
|
|
|
-- [MessageId] - SndMessage ids inside MsgBatch, or single message id
|
2025-02-15 00:12:32 +04:00
|
|
|
type ChatMsgReq = (Connection, MsgFlags, (ValueOrRef MsgBody, [MessageId]))
|
2024-12-20 16:54:24 +04:00
|
|
|
|
|
|
|
deliverMessages :: NonEmpty ChatMsgReq -> CM (NonEmpty (Either ChatError ([Int64], PQEncryption)))
|
|
|
|
deliverMessages msgs = deliverMessagesB $ L.map Right msgs
|
|
|
|
|
|
|
|
deliverMessagesB :: NonEmpty (Either ChatError ChatMsgReq) -> CM (NonEmpty (Either ChatError ([Int64], PQEncryption)))
|
|
|
|
deliverMessagesB msgReqs = do
|
2025-02-15 00:12:32 +04:00
|
|
|
msgReqs' <- if any connSupportsPQ msgReqs then liftIO compressBodies else pure msgReqs
|
2024-12-20 16:54:24 +04:00
|
|
|
sent <- L.zipWith prepareBatch msgReqs' <$> withAgent (`sendMessagesB` snd (mapAccumL toAgent Nothing msgReqs'))
|
|
|
|
lift . void $ withStoreBatch' $ \db -> map (updatePQSndEnabled db) (rights . L.toList $ sent)
|
|
|
|
lift . withStoreBatch $ \db -> L.map (bindRight $ createDelivery db) sent
|
|
|
|
where
|
2025-02-15 00:12:32 +04:00
|
|
|
connSupportsPQ = \case
|
|
|
|
Right (Connection {pqSupport = PQSupportOn, connChatVersion = v}, _, _) -> v >= pqEncryptionCompressionVersion
|
|
|
|
_ -> False
|
2024-12-20 16:54:24 +04:00
|
|
|
compressBodies =
|
2025-02-15 00:12:32 +04:00
|
|
|
forME msgReqs $ \(conn, msgFlags, (mbr, msgIds)) -> runExceptT $ do
|
|
|
|
mbr' <- case mbr of
|
|
|
|
VRValue i msgBody | B.length msgBody > maxCompressedMsgLength -> do
|
2024-12-20 16:54:24 +04:00
|
|
|
let msgBody' = compressedBatchMsgBody_ msgBody
|
|
|
|
when (B.length msgBody' > maxCompressedMsgLength) $ throwError $ ChatError $ CEException "large compressed message"
|
2025-02-15 00:12:32 +04:00
|
|
|
pure $ VRValue i msgBody'
|
|
|
|
v -> pure v
|
|
|
|
pure (conn, msgFlags, (mbr', msgIds))
|
2024-12-20 16:54:24 +04:00
|
|
|
toAgent prev = \case
|
2025-02-15 00:12:32 +04:00
|
|
|
Right (conn@Connection {connId, pqEncryption}, msgFlags, (mbr, _msgIds)) ->
|
2024-12-20 16:54:24 +04:00
|
|
|
let cId = case prev of
|
|
|
|
Just prevId | prevId == connId -> ""
|
|
|
|
_ -> aConnId conn
|
2025-02-15 00:12:32 +04:00
|
|
|
in (Just connId, Right (cId, pqEncryption, msgFlags, mbr))
|
2024-12-20 16:54:24 +04:00
|
|
|
Left _ce -> (prev, Left (AP.INTERNAL "ChatError, skip")) -- as long as it is Left, the agent batchers should just step over it
|
|
|
|
prepareBatch (Right req) (Right ar) = Right (req, ar)
|
|
|
|
prepareBatch (Left ce) _ = Left ce -- restore original ChatError
|
|
|
|
prepareBatch _ (Left ae) = Left $ ChatErrorAgent ae Nothing
|
|
|
|
createDelivery :: DB.Connection -> (ChatMsgReq, (AgentMsgId, PQEncryption)) -> IO (Either ChatError ([Int64], PQEncryption))
|
2025-02-15 00:12:32 +04:00
|
|
|
createDelivery db ((Connection {connId}, _, (_, msgIds)), (agentMsgId, pqEnc')) = do
|
2024-12-20 16:54:24 +04:00
|
|
|
Right . (,pqEnc') <$> mapM (createSndMsgDelivery db (SndMsgDelivery {connId, agentMsgId})) msgIds
|
|
|
|
updatePQSndEnabled :: DB.Connection -> (ChatMsgReq, (AgentMsgId, PQEncryption)) -> IO ()
|
2025-02-15 00:12:32 +04:00
|
|
|
updatePQSndEnabled db ((Connection {connId, pqSndEnabled}, _, _), (_, pqSndEnabled')) =
|
2024-12-20 16:54:24 +04:00
|
|
|
case (pqSndEnabled, pqSndEnabled') of
|
|
|
|
(Just b, b') | b' /= b -> updatePQ
|
|
|
|
(Nothing, PQEncOn) -> updatePQ
|
|
|
|
_ -> pure ()
|
|
|
|
where
|
|
|
|
updatePQ = updateConnPQSndEnabled db connId pqSndEnabled'
|
|
|
|
|
|
|
|
sendGroupMessage :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> CM SndMessage
|
|
|
|
sendGroupMessage user gInfo members chatMsgEvent = do
|
|
|
|
sendGroupMessages user gInfo members (chatMsgEvent :| []) >>= \case
|
|
|
|
((Right msg) :| [], _) -> pure msg
|
|
|
|
_ -> throwChatError $ CEInternalError "sendGroupMessage: expected 1 message"
|
|
|
|
|
|
|
|
sendGroupMessage' :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> CM SndMessage
|
|
|
|
sendGroupMessage' user gInfo members chatMsgEvent =
|
|
|
|
sendGroupMessages_ user gInfo members (chatMsgEvent :| []) >>= \case
|
|
|
|
((Right msg) :| [], _) -> pure msg
|
|
|
|
_ -> throwChatError $ CEInternalError "sendGroupMessage': expected 1 message"
|
|
|
|
|
|
|
|
sendGroupMessages :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> NonEmpty (ChatMsgEvent e) -> CM (NonEmpty (Either ChatError SndMessage), GroupSndResult)
|
|
|
|
sendGroupMessages user gInfo members events = do
|
|
|
|
when shouldSendProfileUpdate $
|
|
|
|
sendProfileUpdate `catchChatError` (toView . CRChatError (Just user))
|
|
|
|
sendGroupMessages_ user gInfo members events
|
|
|
|
where
|
|
|
|
User {profile = p, userMemberProfileUpdatedAt} = user
|
|
|
|
GroupInfo {userMemberProfileSentAt} = gInfo
|
|
|
|
shouldSendProfileUpdate
|
|
|
|
| incognitoMembership gInfo = False
|
|
|
|
| otherwise =
|
|
|
|
case (userMemberProfileSentAt, userMemberProfileUpdatedAt) of
|
|
|
|
(Just lastSentTs, Just lastUpdateTs) -> lastSentTs < lastUpdateTs
|
|
|
|
(Nothing, Just _) -> True
|
|
|
|
_ -> False
|
|
|
|
sendProfileUpdate = do
|
|
|
|
let members' = filter (`supportsVersion` memberProfileUpdateVersion) members
|
|
|
|
profileUpdateEvent = XInfo $ redactedMemberProfile $ fromLocalProfile p
|
|
|
|
void $ sendGroupMessage' user gInfo members' profileUpdateEvent
|
|
|
|
currentTs <- liftIO getCurrentTime
|
|
|
|
withStore' $ \db -> updateUserMemberProfileSentAt db user gInfo currentTs
|
|
|
|
|
|
|
|
data GroupSndResult = GroupSndResult
|
|
|
|
{ sentTo :: [(GroupMemberId, Either ChatError [MessageId], Either ChatError ([Int64], PQEncryption))],
|
|
|
|
pending :: [(GroupMemberId, Either ChatError MessageId, Either ChatError ())],
|
|
|
|
forwarded :: [GroupMember]
|
|
|
|
}
|
|
|
|
|
|
|
|
sendGroupMessages_ :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> NonEmpty (ChatMsgEvent e) -> CM (NonEmpty (Either ChatError SndMessage), GroupSndResult)
|
|
|
|
sendGroupMessages_ _user gInfo@GroupInfo {groupId} members events = do
|
|
|
|
let idsEvts = L.map (GroupId groupId,) events
|
|
|
|
sndMsgs_ <- lift $ createSndMessages idsEvts
|
|
|
|
recipientMembers <- liftIO $ shuffleMembers (filter memberCurrent members)
|
|
|
|
let msgFlags = MsgFlags {notification = any (hasNotification . toCMEventTag) events}
|
|
|
|
(toSendSeparate, toSendBatched, toPending, forwarded, _, dups) =
|
|
|
|
foldr' addMember ([], [], [], [], S.empty, 0 :: Int) recipientMembers
|
|
|
|
when (dups /= 0) $ logError $ "sendGroupMessages_: " <> tshow dups <> " duplicate members"
|
|
|
|
-- TODO PQ either somehow ensure that group members connections cannot have pqSupport/pqEncryption or pass Off's here
|
|
|
|
-- Deliver to toSend members
|
|
|
|
let (sendToMemIds, msgReqs) = prepareMsgReqs msgFlags sndMsgs_ toSendSeparate toSendBatched
|
|
|
|
delivered <- maybe (pure []) (fmap L.toList . deliverMessagesB) $ L.nonEmpty msgReqs
|
|
|
|
when (length delivered /= length sendToMemIds) $ logError "sendGroupMessages_: sendToMemIds and delivered length mismatch"
|
|
|
|
-- Save as pending for toPending members
|
|
|
|
let (pendingMemIds, pendingReqs) = preparePending sndMsgs_ toPending
|
|
|
|
stored <- lift $ withStoreBatch (\db -> map (bindRight $ createPendingMsg db) pendingReqs)
|
|
|
|
when (length stored /= length pendingMemIds) $ logError "sendGroupMessages_: pendingMemIds and stored length mismatch"
|
|
|
|
-- Zip for easier access to results
|
2025-02-15 00:12:32 +04:00
|
|
|
let sentTo = zipWith3 (\mId mReq r -> (mId, fmap (\(_, _, (_, msgIds)) -> msgIds) mReq, r)) sendToMemIds msgReqs delivered
|
2024-12-20 16:54:24 +04:00
|
|
|
pending = zipWith3 (\mId pReq r -> (mId, fmap snd pReq, r)) pendingMemIds pendingReqs stored
|
|
|
|
pure (sndMsgs_, GroupSndResult {sentTo, pending, forwarded})
|
|
|
|
where
|
|
|
|
shuffleMembers :: [GroupMember] -> IO [GroupMember]
|
|
|
|
shuffleMembers ms = do
|
|
|
|
let (adminMs, otherMs) = partition isAdmin ms
|
|
|
|
liftM2 (<>) (shuffle adminMs) (shuffle otherMs)
|
|
|
|
where
|
|
|
|
isAdmin GroupMember {memberRole} = memberRole >= GRAdmin
|
|
|
|
addMember m acc@(toSendSeparate, toSendBatched, pending, forwarded, !mIds, !dups) =
|
|
|
|
case memberSendAction gInfo events members m of
|
|
|
|
Just a
|
|
|
|
| mId `S.member` mIds -> (toSendSeparate, toSendBatched, pending, forwarded, mIds, dups + 1)
|
|
|
|
| otherwise -> case a of
|
|
|
|
MSASend conn -> ((m, conn) : toSendSeparate, toSendBatched, pending, forwarded, mIds', dups)
|
|
|
|
MSASendBatched conn -> (toSendSeparate, (m, conn) : toSendBatched, pending, forwarded, mIds', dups)
|
|
|
|
MSAPending -> (toSendSeparate, toSendBatched, m : pending, forwarded, mIds', dups)
|
|
|
|
MSAForwarded -> (toSendSeparate, toSendBatched, pending, m : forwarded, mIds', dups)
|
|
|
|
Nothing -> acc
|
|
|
|
where
|
|
|
|
mId = groupMemberId' m
|
|
|
|
mIds' = S.insert mId mIds
|
|
|
|
prepareMsgReqs :: MsgFlags -> NonEmpty (Either ChatError SndMessage) -> [(GroupMember, Connection)] -> [(GroupMember, Connection)] -> ([GroupMemberId], [Either ChatError ChatMsgReq])
|
2025-02-15 00:12:32 +04:00
|
|
|
prepareMsgReqs msgFlags msgs toSendSeparate toSendBatched = do
|
|
|
|
let batched_ = batchSndMessagesJSON msgs
|
2024-12-20 16:54:24 +04:00
|
|
|
case L.nonEmpty batched_ of
|
|
|
|
Just batched' -> do
|
2025-02-15 00:12:32 +04:00
|
|
|
let lenMsgs = length msgs
|
|
|
|
(memsSep, mreqsSep) = foldMembers lenMsgs sndMessageMBR msgs toSendSeparate
|
|
|
|
(memsBtch, mreqsBtch) = foldMembers (length batched' + lenMsgs) msgBatchMBR batched' toSendBatched
|
2024-12-20 16:54:24 +04:00
|
|
|
(memsSep <> memsBtch, mreqsSep <> mreqsBtch)
|
|
|
|
Nothing -> ([], [])
|
|
|
|
where
|
2025-02-15 00:12:32 +04:00
|
|
|
foldMembers :: forall a. Int -> (Maybe Int -> Int -> a -> (ValueOrRef MsgBody, [MessageId])) -> NonEmpty (Either ChatError a) -> [(GroupMember, Connection)] -> ([GroupMemberId], [Either ChatError ChatMsgReq])
|
|
|
|
foldMembers lastRef mkMb mbs mems = snd $ foldr' foldMsgBodies (lastMemIdx_, ([], [])) mems
|
2024-12-20 16:54:24 +04:00
|
|
|
where
|
2025-02-15 00:12:32 +04:00
|
|
|
lastMemIdx_ = let len = length mems in if len > 1 then Just len else Nothing
|
|
|
|
foldMsgBodies :: (GroupMember, Connection) -> (Maybe Int, ([GroupMemberId], [Either ChatError ChatMsgReq])) -> (Maybe Int, ([GroupMemberId], [Either ChatError ChatMsgReq]))
|
|
|
|
foldMsgBodies (GroupMember {groupMemberId}, conn) (memIdx_, memIdsReqs) =
|
|
|
|
(subtract 1 <$> memIdx_,) $ snd $ foldr' addBody (lastRef, memIdsReqs) mbs
|
|
|
|
where
|
|
|
|
addBody :: Either ChatError a -> (Int, ([GroupMemberId], [Either ChatError ChatMsgReq])) -> (Int, ([GroupMemberId], [Either ChatError ChatMsgReq]))
|
2025-02-25 14:05:49 +04:00
|
|
|
addBody mb (i, (memIds, reqs)) =
|
2025-02-15 00:12:32 +04:00
|
|
|
let req = (conn,msgFlags,) . mkMb memIdx_ i <$> mb
|
|
|
|
in (i - 1, (groupMemberId : memIds, req : reqs))
|
|
|
|
sndMessageMBR :: Maybe Int -> Int -> SndMessage -> (ValueOrRef MsgBody, [MessageId])
|
|
|
|
sndMessageMBR memIdx_ i SndMessage {msgId, msgBody} = (vrValue_ memIdx_ i msgBody, [msgId])
|
|
|
|
msgBatchMBR :: Maybe Int -> Int -> MsgBatch -> (ValueOrRef MsgBody, [MessageId])
|
|
|
|
msgBatchMBR memIdx_ i (MsgBatch batchBody sndMsgs) = (vrValue_ memIdx_ i batchBody, map (\SndMessage {msgId} -> msgId) sndMsgs)
|
|
|
|
vrValue_ memIdx_ i v = case memIdx_ of
|
|
|
|
Nothing -> VRValue Nothing v -- sending to one member, do not reference bodies
|
|
|
|
Just 1 -> VRValue (Just i) v
|
|
|
|
Just _ -> VRRef i
|
2024-12-20 16:54:24 +04:00
|
|
|
preparePending :: NonEmpty (Either ChatError SndMessage) -> [GroupMember] -> ([GroupMemberId], [Either ChatError (GroupMemberId, MessageId)])
|
|
|
|
preparePending msgs_ =
|
|
|
|
foldr' foldMsgs ([], [])
|
|
|
|
where
|
|
|
|
foldMsgs :: GroupMember -> ([GroupMemberId], [Either ChatError (GroupMemberId, MessageId)]) -> ([GroupMemberId], [Either ChatError (GroupMemberId, MessageId)])
|
|
|
|
foldMsgs GroupMember {groupMemberId} memIdsReqs =
|
|
|
|
foldr' (\msg_ (memIds, reqs) -> (groupMemberId : memIds, fmap pendingReq msg_ : reqs)) memIdsReqs msgs_
|
|
|
|
where
|
|
|
|
pendingReq :: SndMessage -> (GroupMemberId, MessageId)
|
|
|
|
pendingReq SndMessage {msgId} = (groupMemberId, msgId)
|
|
|
|
createPendingMsg :: DB.Connection -> (GroupMemberId, MessageId) -> IO (Either ChatError ())
|
|
|
|
createPendingMsg db (groupMemberId, msgId) =
|
|
|
|
createPendingGroupMessage db groupMemberId msgId Nothing $> Right ()
|
|
|
|
|
|
|
|
data MemberSendAction = MSASend Connection | MSASendBatched Connection | MSAPending | MSAForwarded
|
|
|
|
|
|
|
|
memberSendAction :: GroupInfo -> NonEmpty (ChatMsgEvent e) -> [GroupMember] -> GroupMember -> Maybe MemberSendAction
|
2025-02-25 14:05:49 +04:00
|
|
|
memberSendAction gInfo events members m@GroupMember {memberRole, memberStatus} = case memberConn m of
|
2024-12-20 16:54:24 +04:00
|
|
|
Nothing -> pendingOrForwarded
|
|
|
|
Just conn@Connection {connStatus}
|
2025-02-25 14:05:49 +04:00
|
|
|
| connDisabled conn || connStatus == ConnDeleted || memberStatus == GSMemRejected -> Nothing
|
2024-12-20 16:54:24 +04:00
|
|
|
| connInactive conn -> Just MSAPending
|
|
|
|
| connStatus == ConnSndReady || connStatus == ConnReady -> sendBatchedOrSeparate conn
|
|
|
|
| otherwise -> pendingOrForwarded
|
|
|
|
where
|
|
|
|
sendBatchedOrSeparate conn
|
|
|
|
-- admin doesn't support batch forwarding - send messages separately so that admin can forward one by one
|
|
|
|
| memberRole >= GRAdmin && not (m `supportsVersion` batchSend2Version) = Just (MSASend conn)
|
|
|
|
-- either member is not admin, or admin supports batched forwarding
|
|
|
|
| otherwise = Just (MSASendBatched conn)
|
|
|
|
pendingOrForwarded = case memberCategory m of
|
|
|
|
GCUserMember -> Nothing -- shouldn't happen
|
|
|
|
GCInviteeMember -> Just MSAPending
|
|
|
|
GCHostMember -> Just MSAPending
|
|
|
|
GCPreMember -> forwardSupportedOrPending (invitedByGroupMemberId $ membership gInfo)
|
|
|
|
GCPostMember -> forwardSupportedOrPending (invitedByGroupMemberId m)
|
|
|
|
where
|
|
|
|
forwardSupportedOrPending invitingMemberId_
|
|
|
|
| membersSupport && all isForwardedGroupMsg events = Just MSAForwarded
|
|
|
|
| any isXGrpMsgForward events = Nothing
|
|
|
|
| otherwise = Just MSAPending
|
|
|
|
where
|
|
|
|
membersSupport =
|
|
|
|
m `supportsVersion` groupForwardVersion && invitingMemberSupportsForward
|
|
|
|
invitingMemberSupportsForward = case invitingMemberId_ of
|
|
|
|
Just invMemberId ->
|
|
|
|
-- can be optimized for large groups by replacing [GroupMember] with Map GroupMemberId GroupMember
|
|
|
|
case find (\m' -> groupMemberId' m' == invMemberId) members of
|
|
|
|
Just invitingMember -> invitingMember `supportsVersion` groupForwardVersion
|
|
|
|
Nothing -> False
|
|
|
|
Nothing -> False
|
|
|
|
isXGrpMsgForward event = case event of
|
|
|
|
XGrpMsgForward {} -> True
|
|
|
|
_ -> False
|
|
|
|
|
|
|
|
sendGroupMemberMessage :: MsgEncodingI e => User -> GroupInfo -> GroupMember -> ChatMsgEvent e -> Maybe Int64 -> CM () -> CM ()
|
|
|
|
sendGroupMemberMessage user gInfo@GroupInfo {groupId} m@GroupMember {groupMemberId} chatMsgEvent introId_ postDeliver = do
|
|
|
|
msg <- createSndMessage chatMsgEvent (GroupId groupId)
|
|
|
|
messageMember msg `catchChatError` (toView . CRChatError (Just user))
|
|
|
|
where
|
|
|
|
messageMember :: SndMessage -> CM ()
|
|
|
|
messageMember SndMessage {msgId, msgBody} = forM_ (memberSendAction gInfo (chatMsgEvent :| []) [m] m) $ \case
|
|
|
|
MSASend conn -> deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId >> postDeliver
|
|
|
|
MSASendBatched conn -> deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId >> postDeliver
|
|
|
|
MSAPending -> withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId introId_
|
|
|
|
MSAForwarded -> pure ()
|
|
|
|
|
|
|
|
-- TODO ensure order - pending messages interleave with user input messages
|
|
|
|
sendPendingGroupMessages :: User -> GroupMember -> Connection -> CM ()
|
|
|
|
sendPendingGroupMessages user GroupMember {groupMemberId} conn = do
|
|
|
|
pgms <- withStore' $ \db -> getPendingGroupMessages db groupMemberId
|
|
|
|
forM_ (L.nonEmpty pgms) $ \pgms' -> do
|
|
|
|
let msgs = L.map (\(sndMsg, _, _) -> sndMsg) pgms'
|
|
|
|
void $ batchSendConnMessages user conn MsgFlags {notification = True} msgs
|
|
|
|
lift . void . withStoreBatch' $ \db -> L.map (\SndMessage {msgId} -> deletePendingGroupMessage db groupMemberId msgId) msgs
|
|
|
|
lift . void . withStoreBatch' $ \db -> L.map (\(_, tag, introId_) -> updateIntro_ db tag introId_) pgms'
|
|
|
|
where
|
|
|
|
updateIntro_ :: DB.Connection -> ACMEventTag -> Maybe Int64 -> IO ()
|
|
|
|
updateIntro_ db tag introId_ = case (tag, introId_) of
|
|
|
|
(ACMEventTag _ XGrpMemFwd_, Just introId) -> updateIntroStatus db introId GMIntroInvForwarded
|
|
|
|
_ -> pure ()
|
|
|
|
|
|
|
|
saveDirectRcvMSG :: MsgEncodingI e => Connection -> MsgMeta -> MsgBody -> ChatMessage e -> CM (Connection, RcvMessage)
|
|
|
|
saveDirectRcvMSG conn@Connection {connId} agentMsgMeta msgBody ChatMessage {chatVRange, msgId = sharedMsgId_, chatMsgEvent} = do
|
|
|
|
conn' <- updatePeerChatVRange conn chatVRange
|
|
|
|
let agentMsgId = fst $ recipient agentMsgMeta
|
|
|
|
newMsg = NewRcvMessage {chatMsgEvent, msgBody}
|
|
|
|
rcvMsgDelivery = RcvMsgDelivery {connId, agentMsgId, agentMsgMeta}
|
|
|
|
msg <- withStore $ \db -> createNewMessageAndRcvMsgDelivery db (ConnectionId connId) newMsg sharedMsgId_ rcvMsgDelivery Nothing
|
|
|
|
pure (conn', msg)
|
|
|
|
|
|
|
|
saveGroupRcvMsg :: MsgEncodingI e => User -> GroupId -> GroupMember -> Connection -> MsgMeta -> MsgBody -> ChatMessage e -> CM (GroupMember, Connection, RcvMessage)
|
|
|
|
saveGroupRcvMsg user groupId authorMember conn@Connection {connId} agentMsgMeta msgBody ChatMessage {chatVRange, msgId = sharedMsgId_, chatMsgEvent} = do
|
|
|
|
(am'@GroupMember {memberId = amMemId, groupMemberId = amGroupMemId}, conn') <- updateMemberChatVRange authorMember conn chatVRange
|
|
|
|
let agentMsgId = fst $ recipient agentMsgMeta
|
|
|
|
newMsg = NewRcvMessage {chatMsgEvent, msgBody}
|
|
|
|
rcvMsgDelivery = RcvMsgDelivery {connId, agentMsgId, agentMsgMeta}
|
|
|
|
msg <-
|
|
|
|
withStore (\db -> createNewMessageAndRcvMsgDelivery db (GroupId groupId) newMsg sharedMsgId_ rcvMsgDelivery $ Just amGroupMemId)
|
|
|
|
`catchChatError` \e -> case e of
|
|
|
|
ChatErrorStore (SEDuplicateGroupMessage _ _ _ (Just forwardedByGroupMemberId)) -> do
|
|
|
|
vr <- chatVersionRange
|
|
|
|
fm <- withStore $ \db -> getGroupMember db vr user groupId forwardedByGroupMemberId
|
|
|
|
forM_ (memberConn fm) $ \fmConn ->
|
|
|
|
void $ sendDirectMemberMessage fmConn (XGrpMemCon amMemId) groupId
|
|
|
|
throwError e
|
|
|
|
_ -> throwError e
|
|
|
|
pure (am', conn', msg)
|
|
|
|
|
|
|
|
saveGroupFwdRcvMsg :: MsgEncodingI e => User -> GroupId -> GroupMember -> GroupMember -> MsgBody -> ChatMessage e -> CM RcvMessage
|
|
|
|
saveGroupFwdRcvMsg user groupId forwardingMember refAuthorMember@GroupMember {memberId = refMemberId} msgBody ChatMessage {msgId = sharedMsgId_, chatMsgEvent} = do
|
|
|
|
let newMsg = NewRcvMessage {chatMsgEvent, msgBody}
|
|
|
|
fwdMemberId = Just $ groupMemberId' forwardingMember
|
|
|
|
refAuthorId = Just $ groupMemberId' refAuthorMember
|
|
|
|
withStore (\db -> createNewRcvMessage db (GroupId groupId) newMsg sharedMsgId_ refAuthorId fwdMemberId)
|
|
|
|
`catchChatError` \e -> case e of
|
|
|
|
ChatErrorStore (SEDuplicateGroupMessage _ _ (Just authorGroupMemberId) Nothing) -> do
|
|
|
|
vr <- chatVersionRange
|
|
|
|
am@GroupMember {memberId = amMemberId} <- withStore $ \db -> getGroupMember db vr user groupId authorGroupMemberId
|
|
|
|
if sameMemberId refMemberId am
|
|
|
|
then forM_ (memberConn forwardingMember) $ \fmConn ->
|
|
|
|
void $ sendDirectMemberMessage fmConn (XGrpMemCon amMemberId) groupId
|
|
|
|
else toView $ CRMessageError user "error" "saveGroupFwdRcvMsg: referenced author member id doesn't match message member id"
|
|
|
|
throwError e
|
|
|
|
_ -> throwError e
|
|
|
|
|
|
|
|
saveSndChatItem :: ChatTypeI c => User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> CM (ChatItem c 'MDSnd)
|
|
|
|
saveSndChatItem user cd msg content = saveSndChatItem' user cd msg content Nothing Nothing Nothing Nothing False
|
|
|
|
|
2025-01-29 13:04:48 +00:00
|
|
|
-- TODO [mentions] optimize by avoiding unnecesary parsing of control messages
|
2024-12-20 16:54:24 +04:00
|
|
|
saveSndChatItem' :: ChatTypeI c => User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> Maybe (CIFile 'MDSnd) -> Maybe (CIQuote c) -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> CM (ChatItem c 'MDSnd)
|
2025-01-29 13:04:48 +00:00
|
|
|
saveSndChatItem' user cd msg content ciFile quotedItem itemForwarded itemTimed live = do
|
|
|
|
let itemTexts = ciContentTexts content
|
2025-01-30 10:06:26 +00:00
|
|
|
saveSndChatItems user cd [Right NewSndChatItemData {msg, content, itemTexts, itemMentions = M.empty, ciFile, quotedItem, itemForwarded}] itemTimed live >>= \case
|
2024-12-20 16:54:24 +04:00
|
|
|
[Right ci] -> pure ci
|
|
|
|
_ -> throwChatError $ CEInternalError "saveSndChatItem': expected 1 item"
|
|
|
|
|
|
|
|
data NewSndChatItemData c = NewSndChatItemData
|
|
|
|
{ msg :: SndMessage,
|
|
|
|
content :: CIContent 'MDSnd,
|
2025-01-29 13:04:48 +00:00
|
|
|
itemTexts :: (Text, Maybe MarkdownList),
|
2025-01-30 10:06:26 +00:00
|
|
|
itemMentions :: Map MemberName CIMention,
|
2024-12-20 16:54:24 +04:00
|
|
|
ciFile :: Maybe (CIFile 'MDSnd),
|
|
|
|
quotedItem :: Maybe (CIQuote c),
|
|
|
|
itemForwarded :: Maybe CIForwardedFrom
|
|
|
|
}
|
|
|
|
|
|
|
|
saveSndChatItems ::
|
|
|
|
forall c.
|
|
|
|
ChatTypeI c =>
|
|
|
|
User ->
|
|
|
|
ChatDirection c 'MDSnd ->
|
|
|
|
[Either ChatError (NewSndChatItemData c)] ->
|
|
|
|
Maybe CITimed ->
|
|
|
|
Bool ->
|
|
|
|
CM [Either ChatError (ChatItem c 'MDSnd)]
|
|
|
|
saveSndChatItems user cd itemsData itemTimed live = do
|
|
|
|
createdAt <- liftIO getCurrentTime
|
|
|
|
when (contactChatDeleted cd || any (\NewSndChatItemData {content} -> ciRequiresAttention content) (rights itemsData)) $
|
|
|
|
withStore' (\db -> updateChatTs db user cd createdAt)
|
|
|
|
lift $ withStoreBatch (\db -> map (bindRight $ createItem db createdAt) itemsData)
|
|
|
|
where
|
|
|
|
createItem :: DB.Connection -> UTCTime -> NewSndChatItemData c -> IO (Either ChatError (ChatItem c 'MDSnd))
|
2025-01-29 13:04:48 +00:00
|
|
|
createItem db createdAt NewSndChatItemData {msg = msg@SndMessage {sharedMsgId}, content, itemTexts, itemMentions, ciFile, quotedItem, itemForwarded} = do
|
2024-12-20 16:54:24 +04:00
|
|
|
ciId <- createNewSndChatItem db user cd msg content quotedItem itemForwarded itemTimed live createdAt
|
|
|
|
forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt
|
2025-01-29 13:04:48 +00:00
|
|
|
let ci = mkChatItem_ cd ciId content itemTexts ciFile quotedItem (Just sharedMsgId) itemForwarded itemTimed live False createdAt Nothing createdAt
|
|
|
|
Right <$> case cd of
|
2025-01-30 10:06:26 +00:00
|
|
|
CDGroupSnd g | not (null itemMentions) -> createGroupCIMentions db g ci itemMentions
|
2025-01-29 13:04:48 +00:00
|
|
|
_ -> pure ci
|
|
|
|
|
|
|
|
saveRcvChatItemNoParse :: (ChatTypeI c, ChatTypeQuotable c) => User -> ChatDirection c 'MDRcv -> RcvMessage -> UTCTime -> CIContent 'MDRcv -> CM (ChatItem c 'MDRcv)
|
|
|
|
saveRcvChatItemNoParse user cd msg brokerTs = saveRcvChatItem user cd msg brokerTs . ciContentNoParse
|
2024-12-20 16:54:24 +04:00
|
|
|
|
2025-01-29 13:04:48 +00:00
|
|
|
saveRcvChatItem :: (ChatTypeI c, ChatTypeQuotable c) => User -> ChatDirection c 'MDRcv -> RcvMessage -> UTCTime -> (CIContent 'MDRcv, (Text, Maybe MarkdownList)) -> CM (ChatItem c 'MDRcv)
|
2024-12-20 16:54:24 +04:00
|
|
|
saveRcvChatItem user cd msg@RcvMessage {sharedMsgId_} brokerTs content =
|
2025-01-29 13:04:48 +00:00
|
|
|
saveRcvChatItem' user cd msg sharedMsgId_ brokerTs content Nothing Nothing False M.empty
|
|
|
|
|
|
|
|
ciContentNoParse :: CIContent 'MDRcv -> (CIContent 'MDRcv, (Text, Maybe MarkdownList))
|
|
|
|
ciContentNoParse content = (content, (ciContentToText content, Nothing))
|
2024-12-20 16:54:24 +04:00
|
|
|
|
2025-01-30 10:06:26 +00:00
|
|
|
saveRcvChatItem' :: (ChatTypeI c, ChatTypeQuotable c) => User -> ChatDirection c 'MDRcv -> RcvMessage -> Maybe SharedMsgId -> UTCTime -> (CIContent 'MDRcv, (Text, Maybe MarkdownList)) -> Maybe (CIFile 'MDRcv) -> Maybe CITimed -> Bool -> Map MemberName MsgMention -> CM (ChatItem c 'MDRcv)
|
2025-01-29 13:04:48 +00:00
|
|
|
saveRcvChatItem' user cd msg@RcvMessage {chatMsgEvent, forwardedByMember} sharedMsgId_ brokerTs (content, (t, ft_)) ciFile itemTimed live mentions = do
|
2024-12-20 16:54:24 +04:00
|
|
|
createdAt <- liftIO getCurrentTime
|
2025-01-29 13:04:48 +00:00
|
|
|
withStore' $ \db -> do
|
2024-12-20 16:54:24 +04:00
|
|
|
when (ciRequiresAttention content || contactChatDeleted cd) $ updateChatTs db user cd createdAt
|
2025-01-30 10:06:26 +00:00
|
|
|
(mentions' :: Map MemberName CIMention, userMention) <- case cd of
|
2025-01-29 13:04:48 +00:00
|
|
|
CDGroupRcv g@GroupInfo {membership} _ -> do
|
2025-01-30 10:06:26 +00:00
|
|
|
mentions' <- getRcvCIMentions db user g ft_ mentions
|
2025-01-29 13:04:48 +00:00
|
|
|
let userReply = case cmToQuotedMsg chatMsgEvent of
|
|
|
|
Just QuotedMsg {msgRef = MsgRef {memberId = Just mId}} -> sameMemberId mId membership
|
|
|
|
_ -> False
|
2025-01-30 10:06:26 +00:00
|
|
|
userMention' = userReply || any (\CIMention {memberId} -> sameMemberId memberId membership) mentions'
|
2025-01-29 13:04:48 +00:00
|
|
|
in pure (mentions', userMention')
|
|
|
|
CDDirectRcv _ -> pure (M.empty, False)
|
|
|
|
(ciId, quotedItem, itemForwarded) <- createNewRcvChatItem db user cd msg sharedMsgId_ content itemTimed live userMention brokerTs createdAt
|
2024-12-20 16:54:24 +04:00
|
|
|
forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt
|
2025-01-29 13:04:48 +00:00
|
|
|
let ci = mkChatItem_ cd ciId content (t, ft_) ciFile quotedItem sharedMsgId_ itemForwarded itemTimed live userMention brokerTs forwardedByMember createdAt
|
|
|
|
case cd of
|
|
|
|
CDGroupRcv g _ | not (null mentions') -> createGroupCIMentions db g ci mentions'
|
|
|
|
_ -> pure ci
|
|
|
|
|
|
|
|
-- TODO [mentions] optimize by avoiding unnecessary parsing
|
|
|
|
mkChatItem :: (ChatTypeI c, MsgDirectionI d) => ChatDirection c d -> ChatItemId -> CIContent d -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> ChatItem c d
|
|
|
|
mkChatItem cd ciId content file quotedItem sharedMsgId itemForwarded itemTimed live userMention itemTs forwardedByMember currentTs =
|
|
|
|
let ts = ciContentTexts content
|
|
|
|
in mkChatItem_ cd ciId content ts file quotedItem sharedMsgId itemForwarded itemTimed live userMention itemTs forwardedByMember currentTs
|
|
|
|
|
|
|
|
mkChatItem_ :: (ChatTypeI c, MsgDirectionI d) => ChatDirection c d -> ChatItemId -> CIContent d -> (Text, Maybe MarkdownList) -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> ChatItem c d
|
|
|
|
mkChatItem_ cd ciId content (itemText, formattedText) file quotedItem sharedMsgId itemForwarded itemTimed live userMention itemTs forwardedByMember currentTs =
|
|
|
|
let itemStatus = ciCreateStatus content
|
|
|
|
meta = mkCIMeta ciId content itemText itemStatus Nothing sharedMsgId itemForwarded Nothing False itemTimed (justTrue live) userMention currentTs itemTs forwardedByMember currentTs currentTs
|
|
|
|
in ChatItem {chatDir = toCIDirection cd, meta, content, mentions = M.empty, formattedText, quotedItem, reactions = [], file}
|
2024-12-20 16:54:24 +04:00
|
|
|
|
|
|
|
createAgentConnectionAsync :: ConnectionModeI c => User -> CommandFunction -> Bool -> SConnectionMode c -> SubscriptionMode -> CM (CommandId, ConnId)
|
|
|
|
createAgentConnectionAsync user cmdFunction enableNtfs cMode subMode = do
|
|
|
|
cmdId <- withStore' $ \db -> createCommand db user Nothing cmdFunction
|
|
|
|
connId <- withAgent $ \a -> createConnectionAsync a (aUserId user) (aCorrId cmdId) enableNtfs cMode IKPQOff subMode
|
|
|
|
pure (cmdId, connId)
|
|
|
|
|
|
|
|
joinAgentConnectionAsync :: User -> Bool -> ConnectionRequestUri c -> ConnInfo -> SubscriptionMode -> CM (CommandId, ConnId)
|
|
|
|
joinAgentConnectionAsync user enableNtfs cReqUri cInfo subMode = do
|
|
|
|
cmdId <- withStore' $ \db -> createCommand db user Nothing CFJoinConn
|
|
|
|
connId <- withAgent $ \a -> joinConnectionAsync a (aUserId user) (aCorrId cmdId) enableNtfs cReqUri cInfo PQSupportOff subMode
|
|
|
|
pure (cmdId, connId)
|
|
|
|
|
|
|
|
allowAgentConnectionAsync :: MsgEncodingI e => User -> Connection -> ConfirmationId -> ChatMsgEvent e -> CM ()
|
|
|
|
allowAgentConnectionAsync user conn@Connection {connId, pqSupport, connChatVersion} confId msg = do
|
|
|
|
cmdId <- withStore' $ \db -> createCommand db user (Just connId) CFAllowConn
|
|
|
|
dm <- encodeConnInfoPQ pqSupport connChatVersion msg
|
|
|
|
withAgent $ \a -> allowConnectionAsync a (aCorrId cmdId) (aConnId conn) confId dm
|
|
|
|
withStore' $ \db -> updateConnectionStatus db conn ConnAccepted
|
|
|
|
|
|
|
|
agentAcceptContactAsync :: MsgEncodingI e => User -> Bool -> InvitationId -> ChatMsgEvent e -> SubscriptionMode -> PQSupport -> VersionChat -> CM (CommandId, ConnId)
|
|
|
|
agentAcceptContactAsync user enableNtfs invId msg subMode pqSup chatV = do
|
|
|
|
cmdId <- withStore' $ \db -> createCommand db user Nothing CFAcceptContact
|
|
|
|
dm <- encodeConnInfoPQ pqSup chatV msg
|
|
|
|
connId <- withAgent $ \a -> acceptContactAsync a (aCorrId cmdId) enableNtfs invId dm pqSup subMode
|
|
|
|
pure (cmdId, connId)
|
|
|
|
|
|
|
|
deleteAgentConnectionAsync :: User -> ConnId -> CM ()
|
|
|
|
deleteAgentConnectionAsync user acId = deleteAgentConnectionAsync' user acId False
|
|
|
|
|
|
|
|
deleteAgentConnectionAsync' :: User -> ConnId -> Bool -> CM ()
|
|
|
|
deleteAgentConnectionAsync' user acId waitDelivery = do
|
|
|
|
withAgent (\a -> deleteConnectionAsync a waitDelivery acId) `catchChatError` (toView . CRChatError (Just user))
|
|
|
|
|
|
|
|
deleteAgentConnectionsAsync :: User -> [ConnId] -> CM ()
|
|
|
|
deleteAgentConnectionsAsync user acIds = deleteAgentConnectionsAsync' user acIds False
|
|
|
|
|
|
|
|
deleteAgentConnectionsAsync' :: User -> [ConnId] -> Bool -> CM ()
|
|
|
|
deleteAgentConnectionsAsync' _ [] _ = pure ()
|
|
|
|
deleteAgentConnectionsAsync' user acIds waitDelivery = do
|
|
|
|
withAgent (\a -> deleteConnectionsAsync a waitDelivery acIds) `catchChatError` (toView . CRChatError (Just user))
|
|
|
|
|
|
|
|
agentXFTPDeleteRcvFile :: RcvFileId -> FileTransferId -> CM ()
|
|
|
|
agentXFTPDeleteRcvFile aFileId fileId = do
|
|
|
|
lift $ withAgent' (`xftpDeleteRcvFile` aFileId)
|
|
|
|
withStore' $ \db -> setRcvFTAgentDeleted db fileId
|
|
|
|
|
|
|
|
agentXFTPDeleteRcvFiles :: [(XFTPRcvFile, FileTransferId)] -> CM' ()
|
|
|
|
agentXFTPDeleteRcvFiles rcvFiles = do
|
|
|
|
let rcvFiles' = filter (not . agentRcvFileDeleted . fst) rcvFiles
|
|
|
|
rfIds = mapMaybe fileIds rcvFiles'
|
|
|
|
withAgent' $ \a -> xftpDeleteRcvFiles a (map fst rfIds)
|
|
|
|
void . withStoreBatch' $ \db -> map (setRcvFTAgentDeleted db . snd) rfIds
|
|
|
|
where
|
|
|
|
fileIds :: (XFTPRcvFile, FileTransferId) -> Maybe (RcvFileId, FileTransferId)
|
|
|
|
fileIds (XFTPRcvFile {agentRcvFileId = Just (AgentRcvFileId aFileId)}, fileId) = Just (aFileId, fileId)
|
|
|
|
fileIds _ = Nothing
|
|
|
|
|
|
|
|
agentXFTPDeleteSndFileRemote :: User -> XFTPSndFile -> FileTransferId -> CM' ()
|
|
|
|
agentXFTPDeleteSndFileRemote user xsf fileId =
|
|
|
|
agentXFTPDeleteSndFilesRemote user [(xsf, fileId)]
|
|
|
|
|
|
|
|
agentXFTPDeleteSndFilesRemote :: User -> [(XFTPSndFile, FileTransferId)] -> CM' ()
|
|
|
|
agentXFTPDeleteSndFilesRemote user sndFiles = do
|
|
|
|
(_errs, redirects) <- partitionEithers <$> withStoreBatch' (\db -> map (lookupFileTransferRedirectMeta db user . snd) sndFiles)
|
|
|
|
let redirects' = mapMaybe mapRedirectMeta $ concat redirects
|
|
|
|
sndFilesAll = redirects' <> sndFiles
|
|
|
|
sndFilesAll' = filter (not . agentSndFileDeleted . fst) sndFilesAll
|
|
|
|
-- while file is being prepared and uploaded, it would not have description available;
|
|
|
|
-- this partitions files into those with and without descriptions -
|
|
|
|
-- files with description are deleted remotely, files without description are deleted internally
|
|
|
|
(sfsNoDescr, sfsWithDescr) <- partitionSndDescr sndFilesAll' [] []
|
|
|
|
withAgent' $ \a -> xftpDeleteSndFilesInternal a sfsNoDescr
|
|
|
|
withAgent' $ \a -> xftpDeleteSndFilesRemote a (aUserId user) sfsWithDescr
|
|
|
|
void . withStoreBatch' $ \db -> map (setSndFTAgentDeleted db user . snd) sndFilesAll'
|
|
|
|
where
|
|
|
|
mapRedirectMeta :: FileTransferMeta -> Maybe (XFTPSndFile, FileTransferId)
|
|
|
|
mapRedirectMeta FileTransferMeta {fileId = fileId, xftpSndFile = Just sndFileRedirect} = Just (sndFileRedirect, fileId)
|
|
|
|
mapRedirectMeta _ = Nothing
|
|
|
|
partitionSndDescr ::
|
|
|
|
[(XFTPSndFile, FileTransferId)] ->
|
|
|
|
[SndFileId] ->
|
|
|
|
[(SndFileId, ValidFileDescription 'FSender)] ->
|
|
|
|
CM' ([SndFileId], [(SndFileId, ValidFileDescription 'FSender)])
|
|
|
|
partitionSndDescr [] filesWithoutDescr filesWithDescr = pure (filesWithoutDescr, filesWithDescr)
|
|
|
|
partitionSndDescr ((XFTPSndFile {agentSndFileId = AgentSndFileId aFileId, privateSndFileDescr}, _) : xsfs) filesWithoutDescr filesWithDescr =
|
|
|
|
case privateSndFileDescr of
|
|
|
|
Nothing -> partitionSndDescr xsfs (aFileId : filesWithoutDescr) filesWithDescr
|
|
|
|
Just sfdText ->
|
|
|
|
tryChatError' (parseFileDescription sfdText) >>= \case
|
|
|
|
Left _ -> partitionSndDescr xsfs (aFileId : filesWithoutDescr) filesWithDescr
|
|
|
|
Right sfd -> partitionSndDescr xsfs filesWithoutDescr ((aFileId, sfd) : filesWithDescr)
|
|
|
|
|
|
|
|
userProfileToSend :: User -> Maybe Profile -> Maybe Contact -> Bool -> Profile
|
|
|
|
userProfileToSend user@User {profile = p} incognitoProfile ct inGroup = do
|
|
|
|
let p' = fromMaybe (fromLocalProfile p) incognitoProfile
|
|
|
|
if inGroup
|
|
|
|
then redactedMemberProfile p'
|
|
|
|
else
|
|
|
|
let userPrefs = maybe (preferences' user) (const Nothing) incognitoProfile
|
|
|
|
in (p' :: Profile) {preferences = Just . toChatPrefs $ mergePreferences (userPreferences <$> ct) userPrefs}
|
|
|
|
|
|
|
|
createRcvFeatureItems :: User -> Contact -> Contact -> CM' ()
|
|
|
|
createRcvFeatureItems user ct ct' =
|
|
|
|
createFeatureItems user ct ct' CDDirectRcv CIRcvChatFeature CIRcvChatPreference contactPreference
|
|
|
|
|
|
|
|
createSndFeatureItems :: User -> Contact -> Contact -> CM' ()
|
|
|
|
createSndFeatureItems user ct ct' =
|
|
|
|
createFeatureItems user ct ct' CDDirectSnd CISndChatFeature CISndChatPreference getPref
|
|
|
|
where
|
|
|
|
getPref ContactUserPreference {userPreference} = case userPreference of
|
|
|
|
CUPContact {preference} -> preference
|
|
|
|
CUPUser {preference} -> preference
|
|
|
|
|
|
|
|
type FeatureContent a d = ChatFeature -> a -> Maybe Int -> CIContent d
|
|
|
|
|
|
|
|
createFeatureItems ::
|
|
|
|
MsgDirectionI d =>
|
|
|
|
User ->
|
|
|
|
Contact ->
|
|
|
|
Contact ->
|
|
|
|
(Contact -> ChatDirection 'CTDirect d) ->
|
|
|
|
FeatureContent PrefEnabled d ->
|
|
|
|
FeatureContent FeatureAllowed d ->
|
|
|
|
(forall f. ContactUserPreference (FeaturePreference f) -> FeaturePreference f) ->
|
|
|
|
CM' ()
|
|
|
|
createFeatureItems user ct ct' = createContactsFeatureItems user [(ct, ct')]
|
|
|
|
|
|
|
|
createContactsFeatureItems ::
|
|
|
|
forall d.
|
|
|
|
MsgDirectionI d =>
|
|
|
|
User ->
|
|
|
|
[(Contact, Contact)] ->
|
|
|
|
(Contact -> ChatDirection 'CTDirect d) ->
|
|
|
|
FeatureContent PrefEnabled d ->
|
|
|
|
FeatureContent FeatureAllowed d ->
|
|
|
|
(forall f. ContactUserPreference (FeaturePreference f) -> FeaturePreference f) ->
|
|
|
|
CM' ()
|
|
|
|
createContactsFeatureItems user cts chatDir ciFeature ciOffer getPref = do
|
|
|
|
let dirsCIContents = map contactChangedFeatures cts
|
|
|
|
(errs, acis) <- partitionEithers <$> createInternalItemsForChats user Nothing dirsCIContents
|
|
|
|
unless (null errs) $ toView' $ CRChatErrors (Just user) errs
|
|
|
|
toView' $ CRNewChatItems user acis
|
|
|
|
where
|
|
|
|
contactChangedFeatures :: (Contact, Contact) -> (ChatDirection 'CTDirect d, [CIContent d])
|
|
|
|
contactChangedFeatures (Contact {mergedPreferences = cups}, ct'@Contact {mergedPreferences = cups'}) = do
|
|
|
|
let contents = mapMaybe (\(ACF f) -> featureCIContent_ f) allChatFeatures
|
|
|
|
(chatDir ct', contents)
|
|
|
|
where
|
|
|
|
featureCIContent_ :: forall f. FeatureI f => SChatFeature f -> Maybe (CIContent d)
|
|
|
|
featureCIContent_ f
|
|
|
|
| state /= state' = Just $ fContent ciFeature state'
|
|
|
|
| prefState /= prefState' = Just $ fContent ciOffer prefState'
|
|
|
|
| otherwise = Nothing
|
|
|
|
where
|
|
|
|
fContent :: FeatureContent a d -> (a, Maybe Int) -> CIContent d
|
|
|
|
fContent ci (s, param) = ci f' s param
|
|
|
|
f' = chatFeature f
|
|
|
|
state = featureState cup
|
|
|
|
state' = featureState cup'
|
|
|
|
prefState = preferenceState $ getPref cup
|
|
|
|
prefState' = preferenceState $ getPref cup'
|
|
|
|
cup = getContactUserPreference f cups
|
|
|
|
cup' = getContactUserPreference f cups'
|
|
|
|
|
|
|
|
createGroupFeatureChangedItems :: MsgDirectionI d => User -> ChatDirection 'CTGroup d -> (GroupFeature -> GroupPreference -> Maybe Int -> Maybe GroupMemberRole -> CIContent d) -> GroupInfo -> GroupInfo -> CM ()
|
|
|
|
createGroupFeatureChangedItems user cd ciContent GroupInfo {fullGroupPreferences = gps} GroupInfo {fullGroupPreferences = gps'} =
|
|
|
|
forM_ allGroupFeatures $ \(AGF f) -> do
|
|
|
|
let state = groupFeatureState $ getGroupPreference f gps
|
|
|
|
pref' = getGroupPreference f gps'
|
|
|
|
state'@(_, param', role') = groupFeatureState pref'
|
|
|
|
when (state /= state') $
|
|
|
|
createInternalChatItem user cd (ciContent (toGroupFeature f) (toGroupPreference pref') param' role') Nothing
|
|
|
|
|
|
|
|
sameGroupProfileInfo :: GroupProfile -> GroupProfile -> Bool
|
|
|
|
sameGroupProfileInfo p p' = p {groupPreferences = Nothing} == p' {groupPreferences = Nothing}
|
|
|
|
|
|
|
|
createGroupFeatureItems :: MsgDirectionI d => User -> ChatDirection 'CTGroup d -> (GroupFeature -> GroupPreference -> Maybe Int -> Maybe GroupMemberRole -> CIContent d) -> GroupInfo -> CM ()
|
|
|
|
createGroupFeatureItems user cd ciContent GroupInfo {fullGroupPreferences} =
|
|
|
|
forM_ allGroupFeatures $ \(AGF f) -> do
|
|
|
|
let p = getGroupPreference f fullGroupPreferences
|
|
|
|
(_, param, role) = groupFeatureState p
|
|
|
|
createInternalChatItem user cd (ciContent (toGroupFeature f) (toGroupPreference p) param role) Nothing
|
|
|
|
|
|
|
|
createInternalChatItem :: (ChatTypeI c, MsgDirectionI d) => User -> ChatDirection c d -> CIContent d -> Maybe UTCTime -> CM ()
|
|
|
|
createInternalChatItem user cd content itemTs_ =
|
|
|
|
lift (createInternalItemsForChats user itemTs_ [(cd, [content])]) >>= \case
|
|
|
|
[Right aci] -> toView $ CRNewChatItems user [aci]
|
|
|
|
[Left e] -> throwError e
|
|
|
|
rs -> throwChatError $ CEInternalError $ "createInternalChatItem: expected 1 result, got " <> show (length rs)
|
|
|
|
|
|
|
|
createInternalItemsForChats ::
|
|
|
|
forall c d.
|
|
|
|
(ChatTypeI c, MsgDirectionI d) =>
|
|
|
|
User ->
|
|
|
|
Maybe UTCTime ->
|
|
|
|
[(ChatDirection c d, [CIContent d])] ->
|
|
|
|
CM' [Either ChatError AChatItem]
|
|
|
|
createInternalItemsForChats user itemTs_ dirsCIContents = do
|
|
|
|
createdAt <- liftIO getCurrentTime
|
|
|
|
let itemTs = fromMaybe createdAt itemTs_
|
|
|
|
void . withStoreBatch' $ \db -> map (uncurry $ updateChat db createdAt) dirsCIContents
|
|
|
|
withStoreBatch' $ \db -> concatMap (uncurry $ createACIs db itemTs createdAt) dirsCIContents
|
|
|
|
where
|
|
|
|
updateChat :: DB.Connection -> UTCTime -> ChatDirection c d -> [CIContent d] -> IO ()
|
|
|
|
updateChat db createdAt cd contents
|
|
|
|
| any ciRequiresAttention contents || contactChatDeleted cd = updateChatTs db user cd createdAt
|
|
|
|
| otherwise = pure ()
|
|
|
|
createACIs :: DB.Connection -> UTCTime -> UTCTime -> ChatDirection c d -> [CIContent d] -> [IO AChatItem]
|
|
|
|
createACIs db itemTs createdAt cd = map $ \content -> do
|
|
|
|
ciId <- createNewChatItemNoMsg db user cd content itemTs createdAt
|
2025-01-29 13:04:48 +00:00
|
|
|
let ci = mkChatItem cd ciId content Nothing Nothing Nothing Nothing Nothing False False itemTs Nothing createdAt
|
2024-12-20 16:54:24 +04:00
|
|
|
pure $ AChatItem (chatTypeI @c) (msgDirection @d) (toChatInfo cd) ci
|
|
|
|
|
|
|
|
createLocalChatItems ::
|
|
|
|
User ->
|
|
|
|
ChatDirection 'CTLocal 'MDSnd ->
|
2025-01-29 13:04:48 +00:00
|
|
|
NonEmpty (CIContent 'MDSnd, Maybe (CIFile 'MDSnd), Maybe CIForwardedFrom, (Text, Maybe MarkdownList)) ->
|
2024-12-20 16:54:24 +04:00
|
|
|
UTCTime ->
|
|
|
|
CM [ChatItem 'CTLocal 'MDSnd]
|
|
|
|
createLocalChatItems user cd itemsData createdAt = do
|
|
|
|
withStore' $ \db -> updateChatTs db user cd createdAt
|
2025-01-29 13:04:48 +00:00
|
|
|
(errs, items) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (createItem db) $ L.toList itemsData)
|
2024-12-20 16:54:24 +04:00
|
|
|
unless (null errs) $ toView $ CRChatErrors (Just user) errs
|
|
|
|
pure items
|
|
|
|
where
|
2025-01-29 13:04:48 +00:00
|
|
|
createItem :: DB.Connection -> (CIContent 'MDSnd, Maybe (CIFile 'MDSnd), Maybe CIForwardedFrom, (Text, Maybe MarkdownList)) -> IO (ChatItem 'CTLocal 'MDSnd)
|
|
|
|
createItem db (content, ciFile, itemForwarded, ts) = do
|
|
|
|
ciId <- createNewChatItem_ db user cd Nothing Nothing content (Nothing, Nothing, Nothing, Nothing, Nothing) itemForwarded Nothing False False createdAt Nothing createdAt
|
2024-12-20 16:54:24 +04:00
|
|
|
forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt
|
2025-01-29 13:04:48 +00:00
|
|
|
pure $ mkChatItem_ cd ciId content ts ciFile Nothing Nothing itemForwarded Nothing False False createdAt Nothing createdAt
|
2024-12-20 16:54:24 +04:00
|
|
|
|
|
|
|
withUser' :: (User -> CM ChatResponse) -> CM ChatResponse
|
|
|
|
withUser' action =
|
|
|
|
asks currentUser
|
|
|
|
>>= readTVarIO
|
|
|
|
>>= maybe (throwChatError CENoActiveUser) run
|
|
|
|
where
|
|
|
|
run u = action u `catchChatError` (pure . CRChatCmdError (Just u))
|
|
|
|
|
|
|
|
withUser :: (User -> CM ChatResponse) -> CM ChatResponse
|
|
|
|
withUser action = withUser' $ \user ->
|
|
|
|
ifM (lift chatStarted) (action user) (throwChatError CEChatNotStarted)
|
|
|
|
|
|
|
|
withUser_ :: CM ChatResponse -> CM ChatResponse
|
|
|
|
withUser_ = withUser . const
|
|
|
|
|
|
|
|
withUserId' :: UserId -> (User -> CM ChatResponse) -> CM ChatResponse
|
|
|
|
withUserId' userId action = withUser' $ \user -> do
|
|
|
|
checkSameUser userId user
|
|
|
|
action user
|
|
|
|
|
|
|
|
withUserId :: UserId -> (User -> CM ChatResponse) -> CM ChatResponse
|
|
|
|
withUserId userId action = withUser $ \user -> do
|
|
|
|
checkSameUser userId user
|
|
|
|
action user
|
|
|
|
|
|
|
|
checkSameUser :: UserId -> User -> CM ()
|
|
|
|
checkSameUser userId User {userId = activeUserId} = when (userId /= activeUserId) $ throwChatError (CEDifferentActiveUser userId activeUserId)
|
|
|
|
|
|
|
|
chatStarted :: CM' Bool
|
|
|
|
chatStarted = fmap isJust . readTVarIO =<< asks agentAsync
|
|
|
|
|
|
|
|
waitChatStartedAndActivated :: CM' ()
|
|
|
|
waitChatStartedAndActivated = do
|
|
|
|
agentStarted <- asks agentAsync
|
|
|
|
chatActivated <- asks chatActivated
|
|
|
|
atomically $ do
|
|
|
|
started <- readTVar agentStarted
|
|
|
|
activated <- readTVar chatActivated
|
|
|
|
unless (isJust started && activated) retry
|
|
|
|
|
|
|
|
chatVersionRange :: CM VersionRangeChat
|
|
|
|
chatVersionRange = lift chatVersionRange'
|
|
|
|
{-# INLINE chatVersionRange #-}
|
|
|
|
|
|
|
|
chatVersionRange' :: CM' VersionRangeChat
|
|
|
|
chatVersionRange' = do
|
|
|
|
ChatConfig {chatVRange} <- asks config
|
|
|
|
pure chatVRange
|
|
|
|
{-# INLINE chatVersionRange' #-}
|
|
|
|
|
|
|
|
adminContactReq :: ConnReqContact
|
|
|
|
adminContactReq =
|
|
|
|
either error id $ strDecode "simplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D"
|
|
|
|
|
|
|
|
simplexTeamContactProfile :: Profile
|
|
|
|
simplexTeamContactProfile =
|
|
|
|
Profile
|
|
|
|
{ displayName = "SimpleX Chat team",
|
|
|
|
fullName = "",
|
|
|
|
image = Just (ImageData "data:image/jpg;base64,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
|
|
|
|
contactLink = Just adminContactReq,
|
|
|
|
preferences = Nothing
|
|
|
|
}
|
|
|
|
|
|
|
|
simplexStatusContactProfile :: Profile
|
|
|
|
simplexStatusContactProfile =
|
|
|
|
Profile
|
|
|
|
{ displayName = "SimpleX-Status",
|
|
|
|
fullName = "",
|
|
|
|
image = Just (ImageData "data:image/jpg;base64,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
|
|
|
|
contactLink = Just (either error id $ strDecode "simplex:/contact/#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FShQuD-rPokbDvkyotKx5NwM8P3oUXHxA%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA6fSx1k9zrOmF0BJpCaTarZvnZpMTAVQhd3RkDQ35KT0%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion"),
|
|
|
|
preferences = Nothing
|
|
|
|
}
|
|
|
|
|
|
|
|
timeItToView :: String -> CM' a -> CM' a
|
|
|
|
timeItToView s action = do
|
|
|
|
t1 <- liftIO getCurrentTime
|
|
|
|
a <- action
|
|
|
|
t2 <- liftIO getCurrentTime
|
|
|
|
let diff = diffToMilliseconds $ diffUTCTime t2 t1
|
|
|
|
toView' $ CRTimedAction s diff
|
|
|
|
pure a
|