Files
simplex-chat/src/Simplex/Chat/Library/Commands.hs
Evgeny f0467aee00 directory service: fix queries (#6539)
* fix directory service queries

* fix

* reduce postgres pool size to 1

* stabilize postgres client tests, remove slow handshake tests

* update simplexmq

* fix test

* test delay
2026-01-04 19:04:32 +00:00

4926 lines
304 KiB
Haskell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{-# LANGUAGE CPP #-}
{-# 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 #-}
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
module Simplex.Chat.Library.Commands where
import Control.Applicative (optional, (<|>))
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 qualified Data.Aeson as J
import Data.Attoparsec.ByteString.Char8 (Parser)
import qualified Data.Attoparsec.ByteString.Char8 as A
import qualified Data.Attoparsec.Combinator as A
import qualified Data.ByteString.Base64 as B64
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Char
import Data.Constraint (Dict (..))
import Data.Either (fromRight, partitionEithers, rights)
import Data.Foldable (foldr')
import Data.Functor (($>))
import Data.Int (Int64)
import Data.List (dropWhileEnd, find, foldl', isSuffixOf, partition, sortOn, zipWith4)
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, listToMaybe, mapMaybe, maybeToList)
import qualified Data.Set as S
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
import Data.Time (NominalDiffTime, addUTCTime, defaultTimeLocale, formatTime)
import Data.Time.Clock (UTCTime, getCurrentTime, nominalDay)
import Data.Type.Equality
import qualified Data.UUID as UUID
import qualified Data.UUID.V4 as V4
import Simplex.Chat.Library.Subscriber
import Simplex.Chat.Call
import Simplex.Chat.Controller
import Simplex.Chat.Files
import Simplex.Chat.Markdown
import Simplex.Chat.Messages
import Simplex.Chat.Messages.CIContent
import Simplex.Chat.Messages.CIContent.Events
import Simplex.Chat.Operators
import Simplex.Chat.Options
import Simplex.Chat.ProfileGenerator (generateRandomProfile)
import Simplex.Chat.Protocol
import Simplex.Chat.Remote
import Simplex.Chat.Remote.Types
import Simplex.Chat.Library.Internal
import Simplex.Chat.Stats
import Simplex.Chat.Store
import Simplex.Chat.Store.AppSettings
import Simplex.Chat.Store.ContactRequest
import Simplex.Chat.Store.Connections
import Simplex.Chat.Store.Delivery
import Simplex.Chat.Store.Direct
import Simplex.Chat.Store.Files
import Simplex.Chat.Store.Groups
import Simplex.Chat.Store.Messages
import Simplex.Chat.Store.NoteFolders
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 (liftIOEither, zipWith3')
import qualified Simplex.Chat.Util as U
import Simplex.FileTransfer.Description (FileDescriptionURI (..), maxFileSize, maxFileSizeHard)
import Simplex.Messaging.Agent
import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..), ServerRoles (..), allRoles)
import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Agent.Store.Entity
import Simplex.Messaging.Agent.Store.Interface (execSQL)
import Simplex.Messaging.Agent.Store.Shared (upMigration)
import qualified Simplex.Messaging.Agent.Store.DB as DB
import Simplex.Messaging.Agent.Store.Interface (getCurrentMigrations)
import Simplex.Messaging.Client (NetworkConfig (..), NetworkRequestMode (..), NetworkTimeout (..), SMPWebPortServers (..), SocksMode (SMAlways), textToHostMode)
import qualified Simplex.Messaging.Crypto as C
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 IKPQOn, pattern PQEncOff, pattern PQSupportOff, pattern PQSupportOn)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (base64P)
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), MsgFlags (..), NtfServer, ProtoServerWithAuth (..), ProtocolServer, ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol)
import Simplex.Messaging.ServiceScheme (ServiceScheme (..))
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport.Client (defaultSocksProxyWithAuth)
import Simplex.Messaging.Util
import Simplex.Messaging.Version
import Simplex.RemoteControl.Invitation (RCInvitation (..), RCSignedInvitation (..))
import Simplex.RemoteControl.Types (RCCtrlAddress (..))
import System.Exit (ExitCode, exitSuccess)
import System.FilePath (takeExtension, takeFileName, (</>))
import System.IO (Handle, IOMode (..))
import System.Random (randomRIO)
import UnliftIO.Async
import UnliftIO.Concurrent (forkIO, threadDelay)
import UnliftIO.Directory
import qualified UnliftIO.Exception as E
import UnliftIO.IO (hClose)
import UnliftIO.STM
#if defined(dbPostgres)
import Data.Bifunctor (bimap, second)
import Simplex.Messaging.Agent.Client (SubInfo (..), getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary)
#else
import Data.Bifunctor (bimap, first, second)
import qualified Data.ByteArray as BA
import qualified Database.SQLite.Simple as SQL
import Simplex.Chat.Archive
import Simplex.Messaging.Agent.Client (SubInfo (..), agentClientStore, getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary)
import Simplex.Messaging.Agent.Store.Common (withConnection)
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
#endif
_defaultNtfServers :: [NtfServer]
_defaultNtfServers =
[ -- "ntf://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@ntf2.simplex.im,5ex3mupcazy3zlky64ab27phjhijpemsiby33qzq3pliejipbtx5xgad.onion"
"ntf://KmpZNNXiVZJx_G2T7jRUmDFxWXM3OAnunz3uLT0tqAA=@ntf3.simplex.im,pxculznuryunjdvtvh6s6szmanyadumpbmvevgdpe4wk5c65unyt4yid.onion",
"ntf://CJ5o7X6fCxj2FFYRU2KuCo70y4jSqz7td2HYhLnXWbU=@ntf4.simplex.im,wtvuhdj26jwprmomnyfu5wfuq2hjkzfcc72u44vi6gdhrwxldt6xauad.onion"
]
maxImageSize :: Integer
maxImageSize = 261120 * 2 -- auto-receive on mobiles
imageExtensions :: [String]
imageExtensions = [".jpg", ".jpeg", ".png", ".gif"]
fixedImagePreview :: ImageData
fixedImagePreview = ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAKVJREFUeF7t1kENACEUQ0FQhnVQ9lfGO+xggITQdvbMzArPey+8fa3tAfwAEdABZQspQStgBssEcgAIkSAJkiAJljtEgiRIgmUCSZAESZAESZAEyx0iQRIkwTKBJEiCv5fgvTd1wDmn7QAP4AeIgA4oW0gJWgEzWCZwbQ7gAA7ggLKFOIADOKBMIAeAEAmSIAmSYLlDJEiCJFgmkARJkARJ8N8S/ADTZUewBvnTOQAAAABJRU5ErkJggg=="
imageFilePrefix :: String
imageFilePrefix = "IMG_"
voiceFilePrefix :: String
voiceFilePrefix = "voice_"
videoFilePrefix :: String
videoFilePrefix = "video_"
-- enableSndFiles has no effect when mainApp is True
startChatController :: Bool -> Bool -> CM' (Async ())
startChatController mainApp enableSndFiles = do
asks smpAgent >>= liftIO . resumeAgentClient
unless mainApp $ chatWriteVar' subscriptionMode SMOnlyCreate
users <- fromRight [] <$> runExceptT (withFastStore' getUsers)
runExceptT (syncConnections' users) >>= \case
Left e -> liftIO $ putStrLn $ "Error synchronizing connections: " <> show e
Right _ -> pure ()
restoreCalls
s <- asks agentAsync
readTVarIO s >>= maybe (start s users) (pure . fst)
where
syncConnections' users =
whenM (withFastStore' shouldSyncConnections) $ do
let aUserIds = map aUserId users
connIds <- concat <$> forM users getConnsToSub
(userDiff, connDiff) <- withAgent (\a -> syncConnections a aUserIds connIds)
withFastStore' setConnectionsSyncTs
toView $ CEvtConnectionsDiff (AgentUserId <$> userDiff) (AgentConnId <$> connDiff)
start s users = do
a1 <- async agentSubscriber
a2 <-
if mainApp
then Just <$> async (subscribeUsers False users)
else pure Nothing
atomically . writeTVar s $ Just (a1, a2)
if mainApp
then do
startXFTP xftpStartWorkers
void $ forkIO $ startFilesToReceive users
startDeliveryWorkers
startCleanupManager
void $ forkIO $ mapM_ startExpireCIs users
else when enableSndFiles $ startXFTP xftpStartSndWorkers
pure a1
startXFTP startWorkers = do
tmp <- readTVarIO =<< asks tempDirectory
runExceptT (withAgent $ \a -> startWorkers a tmp) >>= \case
Left e -> liftIO $ putStrLn $ "Error starting XFTP workers: " <> show e
Right _ -> pure ()
startDeliveryWorkers =
runExceptT (startDeliveryTaskWorkers >> startDeliveryJobWorkers) >>= \case
Left e -> liftIO $ putStrLn $ "Error starting delivery workers: " <> show e
Right _ -> pure ()
startCleanupManager = do
cleanupAsync <- asks cleanupManagerAsync
readTVarIO cleanupAsync >>= \case
Nothing -> do
a <- Just <$> async (void $ runExceptT cleanupManager)
atomically $ writeTVar cleanupAsync a
_ -> pure ()
startExpireCIs user = whenM shouldExpireChats $ do
startExpireCIThread user
setExpireCIFlag user True
where
shouldExpireChats =
fmap (fromRight False) $ runExceptT $ withStore' $ \db -> do
ttl <- getChatItemTTL db user
ttlCount <- getChatTTLCount db user
pure $ ttl > 0 || ttlCount > 0
getConnsToSub :: User -> CM [ConnId]
getConnsToSub user =
withFastStore' $ \db -> do
ctConnIds <- getContactConnsToSub db user False
uclConnIds <- getUCLConnsToSub db user False
memberConnIds <- getMemberConnsToSub db user False
pendingConnIds <- getPendingConnsToSub db user False
pure $ ctConnIds <> uclConnIds <> memberConnIds <> pendingConnIds
subscribeUsers :: Bool -> [User] -> CM' ()
subscribeUsers onlyNeeded users = do
let activeUserId_ = (\User {agentUserId = AgentUserId uId} -> uId) <$> find activeUser users
withAgent (\a -> subscribeAllConnections a onlyNeeded activeUserId_) `catchAllErrors'` eToView'
startFilesToReceive :: [User] -> CM' ()
startFilesToReceive users = do
let (us, us') = partition activeUser users
startReceive us
startReceive us'
where
startReceive :: [User] -> CM' ()
startReceive = mapM_ $ runExceptT . startReceiveUserFiles
startReceiveUserFiles :: User -> CM ()
startReceiveUserFiles user = do
filesToReceive <- withStore' (`getRcvFilesToReceive` user)
forM_ filesToReceive $ \ft ->
flip catchAllErrors eToView $
toView =<< receiveFileEvt' user ft False Nothing Nothing
restoreCalls :: CM' ()
restoreCalls = do
savedCalls <- fromRight [] <$> runExceptT (withFastStore' getCalls)
let callsMap = M.fromList $ map (\call@Call {contactId} -> (contactId, call)) savedCalls
calls <- asks currentCalls
atomically $ writeTVar calls callsMap
stopChatController :: ChatController -> IO ()
stopChatController ChatController {smpAgent, agentAsync = s, sndFiles, rcvFiles, expireCIFlags, remoteHostSessions, remoteCtrlSession} = do
readTVarIO remoteHostSessions >>= mapM_ (cancelRemoteHost False . snd)
atomically (stateTVar remoteCtrlSession (,Nothing)) >>= mapM_ (cancelRemoteCtrl False . snd)
disconnectAgentClient smpAgent
readTVarIO s >>= mapM_ (\(a1, a2) -> forkIO $ uninterruptibleCancel a1 >> mapM_ uninterruptibleCancel a2)
closeFiles sndFiles
closeFiles rcvFiles
atomically $ do
keys <- M.keys <$> readTVar expireCIFlags
forM_ keys $ \k -> TM.insert k False expireCIFlags
writeTVar s Nothing
where
closeFiles :: TVar (Map Int64 Handle) -> IO ()
closeFiles files = do
fs <- readTVarIO files
mapM_ hClose fs
atomically $ writeTVar files M.empty
updateNetworkConfig :: NetworkConfig -> SimpleNetCfg -> NetworkConfig
updateNetworkConfig cfg SimpleNetCfg {socksProxy, socksMode, hostMode, requiredHostMode, smpProxyMode_, smpProxyFallback_, smpWebPortServers, tcpTimeout_, logTLSErrors} =
let cfg1 = maybe cfg (\smpProxyMode -> cfg {smpProxyMode}) smpProxyMode_
cfg2 = maybe cfg1 (\smpProxyFallback -> cfg1 {smpProxyFallback}) smpProxyFallback_
cfg3 = maybe cfg2 (\t -> cfg2 {tcpTimeout = nt t, tcpConnectTimeout = nt ((t * 3) `div` 2)}) tcpTimeout_
nt t = NetworkTimeout {backgroundTimeout = t * 3, interactiveTimeout = t}
in cfg3 {socksProxy, socksMode, hostMode, requiredHostMode, smpWebPortServers, logTLSErrors}
useServers :: Foldable f => RandomAgentServers -> [(Text, ServerOperator)] -> f UserOperatorServers -> (NonEmpty (ServerCfg 'PSMP), NonEmpty (ServerCfg 'PXFTP))
useServers as opDomains uss =
let smp' = useServerCfgs SPSMP as opDomains $ concatMap (servers' SPSMP) uss
xftp' = useServerCfgs SPXFTP as opDomains $ concatMap (servers' SPXFTP) uss
in (smp', xftp')
execChatCommand :: Maybe RemoteHostId -> ByteString -> Int -> CM' (Either ChatError ChatResponse)
execChatCommand rh s retryNum =
case parseChatCommand s of
Left e -> pure $ chatCmdError e
Right cmd -> case rh of
Just rhId
| allowRemoteCommand cmd -> execRemoteCommand rhId cmd s retryNum
| otherwise -> pure $ Left $ ChatErrorRemoteHost (RHId rhId) $ RHELocalCommand
_ -> do
cc@ChatController {config = ChatConfig {chatHooks}} <- ask
case preCmdHook chatHooks of
Just hook -> liftIO (hook cc cmd) >>= either pure (`execChatCommand'` retryNum)
Nothing -> execChatCommand' cmd retryNum
execChatCommand' :: ChatCommand -> Int -> CM' (Either ChatError ChatResponse)
execChatCommand' cmd retryNum = handleCommandError $ do
vr <- chatVersionRange
processChatCommand vr (NRMInteractive' retryNum) cmd
execRemoteCommand :: RemoteHostId -> ChatCommand -> ByteString -> Int -> CM' (Either ChatError ChatResponse)
execRemoteCommand rhId cmd s retryNum = handleCommandError $ getRemoteHostClient rhId >>= \rh -> processRemoteCommand rhId rh cmd s retryNum
handleCommandError :: CM ChatResponse -> CM' (Either ChatError ChatResponse)
handleCommandError a = runExceptT a `E.catches` ioErrors
where
ioErrors =
[ E.Handler $ \(e :: ExitCode) -> E.throwIO e,
E.Handler $ pure . Left . fromSomeException
]
parseChatCommand :: ByteString -> Either String ChatCommand
parseChatCommand = A.parseOnly chatCommandP . B.dropWhileEnd isSpace
-- | Chat API commands interpreted in context of a local zone
processChatCommand :: VersionRangeChat -> NetworkRequestMode -> ChatCommand -> CM ChatResponse
processChatCommand vr nm = \case
ShowActiveUser -> withUser' $ pure . CRActiveUser
CreateActiveUser NewUser {profile, pastTimestamp} -> do
forM_ profile $ \Profile {displayName} -> checkValidName displayName
p@Profile {displayName} <- liftIO $ maybe generateRandomProfile pure profile
u <- asks currentUser
users <- withFastStore' getUsers
forM_ users $ \User {localDisplayName = n, activeUser, viewPwdHash} ->
when (n == displayName) . throwChatError $
if activeUser || isNothing viewPwdHash then CEUserExists displayName else CEInvalidDisplayName {displayName, validName = ""}
(uss, (smp', xftp')) <- chooseServers =<< readTVarIO u
auId <- withAgent $ \a -> createUser a smp' xftp'
ts <- liftIO $ getCurrentTime >>= if pastTimestamp then coupleDaysAgo else pure
user <- withFastStore $ \db -> do
user <- createUserRecordAt db (AgentUserId auId) p True ts
mapM_ (setUserServers db user ts) uss
createPresetContactCards db user `catchAllErrors` \_ -> pure ()
createNoteFolder db user
pure user
atomically . writeTVar u $ Just user
pure $ CRActiveUser user
where
createPresetContactCards :: DB.Connection -> User -> ExceptT StoreError IO ()
createPresetContactCards db user = do
createContact db user simplexStatusContactProfile
createContact db user simplexTeamContactProfile
chooseServers :: Maybe User -> CM ([UpdatedUserOperatorServers], (NonEmpty (ServerCfg 'PSMP), NonEmpty (ServerCfg 'PXFTP)))
chooseServers user_ = do
as <- asks randomAgentServers
mapM (withFastStore . flip getUserServers >=> liftIO . groupByOperator) user_ >>= \case
Just uss -> do
let opDomains = operatorDomains $ mapMaybe operator' uss
uss' = map copyServers uss
pure $ (uss',) $ useServers as opDomains uss
Nothing -> do
ps <- asks randomPresetServers
uss <- presetUserServers <$> withFastStore' (\db -> getUpdateServerOperators db ps True)
let RandomAgentServers {smpServers = smp', xftpServers = xftp'} = as
pure (uss, (smp', xftp'))
copyServers :: UserOperatorServers -> UpdatedUserOperatorServers
copyServers UserOperatorServers {operator, smpServers, xftpServers} =
let new srv = AUS SDBNew srv {serverId = DBNewEntity}
in UpdatedUserOperatorServers {operator, smpServers = map new smpServers, xftpServers = map new xftpServers}
coupleDaysAgo t = (`addUTCTime` t) . fromInteger . negate . (+ (2 * day)) <$> randomRIO (0, day)
day = 86400
ListUsers -> CRUsersList <$> withFastStore' getUsersInfo
APISetActiveUser userId' viewPwd_ -> do
unlessM (lift chatStarted) $ throwChatError CEChatNotStarted
user_ <- chatReadVar currentUser
user' <- privateGetUser userId'
validateUserPassword_ user_ user' viewPwd_
user'' <- withFastStore' (`setActiveUser` user')
chatWriteVar currentUser $ Just user''
pure $ CRActiveUser user''
SetActiveUser uName viewPwd_ -> do
tryAllErrors (withFastStore (`getUserIdByName` uName)) >>= \case
Left _ -> throwChatError CEUserUnknown
Right userId -> processChatCommand vr nm $ APISetActiveUser userId viewPwd_
SetAllContactReceipts onOff -> withUser $ \_ -> withFastStore' (`updateAllContactReceipts` onOff) >> ok_
APISetUserContactReceipts userId' settings -> withUser $ \user -> do
user' <- privateGetUser userId'
validateUserPassword user user' Nothing
withFastStore' $ \db -> updateUserContactReceipts db user' settings
ok user
SetUserContactReceipts settings -> withUser $ \User {userId} -> processChatCommand vr nm $ APISetUserContactReceipts userId settings
APISetUserGroupReceipts userId' settings -> withUser $ \user -> do
user' <- privateGetUser userId'
validateUserPassword user user' Nothing
withFastStore' $ \db -> updateUserGroupReceipts db user' settings
ok user
SetUserGroupReceipts settings -> withUser $ \User {userId} -> processChatCommand vr nm $ APISetUserGroupReceipts userId settings
APISetUserAutoAcceptMemberContacts userId' onOff -> withUser $ \user -> do
user' <- privateGetUser userId'
validateUserPassword user user' Nothing
withFastStore' $ \db -> updateUserAutoAcceptMemberContacts db user' onOff
ok user
SetUserAutoAcceptMemberContacts onOff -> withUser $ \User {userId} -> processChatCommand vr nm $ APISetUserAutoAcceptMemberContacts userId onOff
APIHideUser userId' (UserPwd viewPwd) -> withUser $ \user -> do
user' <- privateGetUser userId'
case viewPwdHash user' of
Just _ -> throwChatError $ CEUserAlreadyHidden userId'
_ -> do
when (T.null viewPwd) $ throwChatError $ CEEmptyUserPassword userId'
users <- withFastStore' getUsers
unless (length (filter (isNothing . viewPwdHash) users) > 1) $ throwChatError $ CECantHideLastUser userId'
viewPwdHash' <- hashPassword
setUserPrivacy user user' {viewPwdHash = viewPwdHash', showNtfs = False}
where
hashPassword = do
salt <- drgRandomBytes 16
let hash = B64UrlByteString $ C.sha512Hash $ encodeUtf8 viewPwd <> salt
pure $ Just UserPwdHash {hash, salt = B64UrlByteString salt}
APIUnhideUser userId' viewPwd@(UserPwd pwd) -> withUser $ \user -> do
user' <- privateGetUser userId'
case viewPwdHash user' of
Nothing -> throwChatError $ CEUserNotHidden userId'
_ -> do
when (T.null pwd) $ throwChatError $ CEEmptyUserPassword userId'
validateUserPassword user user' $ Just viewPwd
setUserPrivacy user user' {viewPwdHash = Nothing, showNtfs = True}
APIMuteUser userId' -> setUserNotifications userId' False
APIUnmuteUser userId' -> setUserNotifications userId' True
HideUser viewPwd -> withUser $ \User {userId} -> processChatCommand vr nm $ APIHideUser userId viewPwd
UnhideUser viewPwd -> withUser $ \User {userId} -> processChatCommand vr nm $ APIUnhideUser userId viewPwd
MuteUser -> withUser $ \User {userId} -> processChatCommand vr nm $ APIMuteUser userId
UnmuteUser -> withUser $ \User {userId} -> processChatCommand vr nm $ APIUnmuteUser userId
APIDeleteUser userId' delSMPQueues viewPwd_ -> withUser $ \user -> do
user' <- privateGetUser userId'
validateUserPassword user user' viewPwd_
checkDeleteChatUser user'
withChatLock "deleteUser" $ deleteChatUser user' delSMPQueues
DeleteUser uName delSMPQueues viewPwd_ -> withUserName uName $ \userId -> APIDeleteUser userId delSMPQueues viewPwd_
StartChat {mainApp, enableSndFiles} -> withUser' $ \_ ->
asks agentAsync >>= readTVarIO >>= \case
Just _ -> pure CRChatRunning
_ -> checkStoreNotChanged . lift $ startChatController mainApp enableSndFiles $> CRChatStarted
CheckChatRunning -> maybe CRChatStopped (const CRChatRunning) <$> chatReadVar agentAsync
APIStopChat -> do
ask >>= liftIO . stopChatController
pure CRChatStopped
APIActivateChat restoreChat -> withUser $ \_ -> do
lift $ when restoreChat restoreCalls
lift $ withAgent' foregroundAgent
chatWriteVar chatActivated True
when restoreChat $ do
users <- withFastStore' getUsers
lift $ do
void . forkIO $ subscribeUsers True users
void . forkIO $ startFilesToReceive users
setAllExpireCIFlags True
ok_
APISuspendChat t -> do
chatWriteVar chatActivated False
lift $ setAllExpireCIFlags False
stopRemoteCtrl
lift $ withAgent' (`suspendAgent` t)
ok_
ShowConnectionsDiff showIds -> do
users <- withFastStore' getUsers
let aUserIds = map aUserId users
connIds <- concat <$> forM users getConnsToSub
(userDiff, connDiff) <- withAgent (\a -> compareConnections a aUserIds connIds)
pure $ CRConnectionsDiff showIds (AgentUserId <$> userDiff) (AgentConnId <$> connDiff)
ResubscribeAllConnections -> withStore' getUsers >>= lift . subscribeUsers False >> ok_
-- has to be called before StartChat
SetTempFolder tf -> do
createDirectoryIfMissing True tf
asks tempDirectory >>= atomically . (`writeTVar` Just tf)
ok_
SetFilesFolder ff -> do
createDirectoryIfMissing True ff
asks filesFolder >>= atomically . (`writeTVar` Just ff)
ok_
SetRemoteHostsFolder rf -> do
createDirectoryIfMissing True rf
chatWriteVar remoteHostsFolder $ Just rf
ok_
-- has to be called before StartChat
APISetAppFilePaths cfg -> do
setFolder filesFolder $ appFilesFolder cfg
setFolder tempDirectory $ appTempFolder cfg
setFolder assetsDirectory $ appAssetsFolder cfg
mapM_ (setFolder remoteHostsFolder) $ appRemoteHostsFolder cfg
ok_
where
setFolder sel f = do
createDirectoryIfMissing True f
chatWriteVar sel $ Just f
APISetEncryptLocalFiles on -> chatWriteVar encryptLocalFiles on >> ok_
SetContactMergeEnabled onOff -> chatWriteVar contactMergeEnabled onOff >> ok_
#if !defined(dbPostgres)
APIExportArchive cfg -> checkChatStopped $ CRArchiveExported <$> lift (exportArchive cfg)
ExportArchive -> do
ts <- liftIO getCurrentTime
let filePath = "simplex-chat." <> formatTime defaultTimeLocale "%FT%H%M%SZ" ts <> ".zip"
processChatCommand vr nm $ APIExportArchive $ ArchiveConfig filePath Nothing Nothing
APIImportArchive cfg -> checkChatStopped $ do
fileErrs <- lift $ importArchive cfg
setStoreChanged
pure $ CRArchiveImported fileErrs
APIDeleteStorage -> withStoreChanged deleteStorage
APIStorageEncryption cfg -> withStoreChanged $ sqlCipherExport cfg
TestStorageEncryption key -> sqlCipherTestKey key >> ok_
SlowSQLQueries -> do
ChatController {chatStore, smpAgent} <- ask
chatQueries <- slowQueries chatStore
agentQueries <- slowQueries $ agentClientStore smpAgent
pure CRSlowSQLQueries {chatQueries, agentQueries}
where
slowQueries st =
liftIO $
map (uncurry SlowSQLQuery . first SQL.fromQuery)
. sortOn (timeAvg . snd)
. M.assocs
<$> withConnection st (readTVarIO . DB.slow)
#endif
ExecChatStoreSQL query -> CRSQLResult <$> withStore' (`execSQL` query)
ExecAgentStoreSQL query -> CRSQLResult <$> withAgent (`execAgentStoreSQL` query)
APISaveAppSettings as -> withFastStore' (`saveAppSettings` as) >> ok_
APIGetAppSettings platformDefaults -> CRAppSettings <$> withFastStore' (`getAppSettings` platformDefaults)
APIGetChatTags userId -> withUserId' userId $ \user -> do
tags <- withFastStore' (`getUserChatTags` user)
pure $ CRChatTags user tags
APIGetChats {userId, pendingConnections, pagination, query} -> withUserId' userId $ \user -> do
(errs, previews) <- partitionEithers <$> withFastStore' (\db -> getChatPreviews db vr user pendingConnections pagination query)
unless (null errs) $ toView $ CEvtChatErrors (map ChatErrorStore errs)
pure $ CRApiChats user previews
APIGetChat (ChatRef cType cId scope_) contentFilter pagination search -> withUser $ \user -> case cType of
-- TODO optimize queries calculating ChatStats, currently they're disabled
CTDirect -> do
when (isJust contentFilter) $ throwCmdError "content filter not supported"
(directChat, navInfo) <- withFastStore (\db -> getDirectChat db vr user cId pagination search)
pure $ CRApiChat user (AChat SCTDirect directChat) navInfo
CTGroup -> do
(groupChat, navInfo) <- withFastStore (\db -> getGroupChat db vr user cId scope_ contentFilter pagination search)
groupChat' <- checkSupportChatAttention user groupChat
pure $ CRApiChat user (AChat SCTGroup groupChat') navInfo
CTLocal -> do
when (isJust contentFilter) $ throwCmdError "content filter not supported"
(localChat, navInfo) <- withFastStore (\db -> getLocalChat db user cId pagination search)
pure $ CRApiChat user (AChat SCTLocal localChat) navInfo
CTContactRequest -> throwCmdError "not implemented"
CTContactConnection -> throwCmdError "not supported"
where
checkSupportChatAttention :: User -> Chat 'CTGroup -> CM (Chat 'CTGroup)
checkSupportChatAttention user groupChat@Chat {chatInfo, chatItems} =
case chatInfo of
GroupChat gInfo (Just GCSIMemberSupport {groupMember_ = Just scopeMem@GroupMember {supportChat = Just suppChat}}) -> do
case correctedMemAttention (groupMemberId' scopeMem) suppChat chatItems of
Just newMemAttention -> do
(gInfo', scopeMem') <-
withFastStore' $ \db -> setSupportChatMemberAttention db vr user gInfo scopeMem newMemAttention
pure (groupChat {chatInfo = GroupChat gInfo' (Just $ GCSIMemberSupport (Just scopeMem'))} :: Chat 'CTGroup)
Nothing -> pure groupChat
_ -> pure groupChat
where
correctedMemAttention :: GroupMemberId -> GroupSupportChat -> [CChatItem 'CTGroup] -> Maybe Int64
correctedMemAttention scopeGMId GroupSupportChat {memberAttention} items =
let numNewFromMember = fromIntegral . length . takeWhile newFromMember $ reverse items
in if numNewFromMember == memberAttention then Nothing else Just numNewFromMember
where
newFromMember :: CChatItem 'CTGroup -> Bool
newFromMember (CChatItem _ ChatItem {chatDir = CIGroupRcv m, meta = CIMeta {itemStatus = CISRcvNew}}) =
groupMemberId' m == scopeGMId
newFromMember _ = False
APIGetChatItems pagination search -> withUser $ \user -> do
chatItems <- withFastStore $ \db -> getAllChatItems db vr user pagination search
pure $ CRChatItems user Nothing chatItems
APIGetChatItemInfo chatRef itemId -> withUser $ \user -> do
(aci@(AChatItem cType dir _ ci), versions) <- withFastStore $ \db ->
(,) <$> getAChatItem db vr user chatRef itemId <*> liftIO (getChatItemVersions db itemId)
let itemVersions = if null versions then maybeToList $ mkItemVersion ci else versions
memberDeliveryStatuses <- case (cType, dir) of
(SCTGroup, SMDSnd) -> L.nonEmpty <$> withFastStore' (`getGroupSndStatuses` itemId)
_ -> pure Nothing
forwardedFromChatItem <- getForwardedFromItem user ci
pure $ CRChatItemInfo user aci ChatItemInfo {itemVersions, memberDeliveryStatuses, forwardedFromChatItem}
where
getForwardedFromItem :: User -> ChatItem c d -> CM (Maybe AChatItem)
getForwardedFromItem user ChatItem {meta = CIMeta {itemForwarded}} = case itemForwarded of
Just (CIFFContact _ _ (Just ctId) (Just fwdItemId)) ->
Just <$> withFastStore (\db -> getAChatItem db vr user (ChatRef CTDirect ctId Nothing) fwdItemId)
Just (CIFFGroup _ _ (Just gId) (Just fwdItemId)) ->
-- TODO [knocking] getAChatItem doesn't differentiate how to read based on scope - it should, instead of using group filter
Just <$> withFastStore (\db -> getAChatItem db vr user (ChatRef CTGroup gId Nothing) fwdItemId)
_ -> pure Nothing
APISendMessages sendRef live itemTTL cms -> withUser $ \user -> mapM_ assertAllowedContent' cms >> case sendRef of
SRDirect chatId -> do
mapM_ assertNoMentions cms
withContactLock "sendMessage" chatId $
sendContactContentMessages user chatId live itemTTL (L.map composedMessageReq cms)
SRGroup chatId gsScope ->
withGroupLock "sendMessage" chatId $ do
(gInfo, cmrs) <- withFastStore $ \db -> do
g <- getGroupInfo db vr user chatId
(g,) <$> mapM (composedMessageReqMentions db user g) cms
sendGroupContentMessages user gInfo gsScope live itemTTL cmrs
APICreateChatTag (ChatTagData emoji text) -> withUser $ \user -> withFastStore' $ \db -> do
_ <- createChatTag db user emoji text
CRChatTags user <$> getUserChatTags db user
APISetChatTags (ChatRef cType chatId scope) tagIds -> withUser $ \user -> case cType of
CTDirect -> withFastStore' $ \db -> do
updateDirectChatTags db chatId (maybe [] L.toList tagIds)
CRTagsUpdated user <$> getUserChatTags db user <*> getDirectChatTags db chatId
CTGroup | isNothing scope -> withFastStore' $ \db -> do
updateGroupChatTags db chatId (maybe [] L.toList tagIds)
CRTagsUpdated user <$> getUserChatTags db user <*> getGroupChatTags db chatId
_ -> throwCmdError "not supported"
APIDeleteChatTag tagId -> withUser $ \user -> do
withFastStore' $ \db -> deleteChatTag db user tagId
ok user
APIUpdateChatTag tagId (ChatTagData emoji text) -> withUser $ \user -> do
withFastStore' $ \db -> updateChatTag db user tagId emoji text
ok user
APIReorderChatTags tagIds -> withUser $ \user -> do
withFastStore' $ \db -> reorderChatTags db user $ L.toList tagIds
ok user
APICreateChatItems folderId cms -> withUser $ \user -> do
forM_ cms $ \cm -> assertAllowedContent' cm >> assertNoMentions cm
createNoteFolderContentItems user folderId (L.map composedMessageReq cms)
APIReportMessage gId reportedItemId reportReason reportText -> withUser $ \user ->
withGroupLock "reportMessage" gId $ do
gInfo <- withFastStore $ \db -> getGroupInfo db vr user gId
let mc = MCReport reportText reportReason
cm = ComposedMessage {fileSource = Nothing, quotedItemId = Just reportedItemId, msgContent = mc, mentions = M.empty}
sendGroupContentMessages user gInfo (Just $ GCSMemberSupport Nothing) False Nothing [composedMessageReq cm]
ReportMessage {groupName, contactName_, reportReason, reportedMessage} -> withUser $ \user -> do
gId <- withFastStore $ \db -> getGroupIdByName db user groupName
reportedItemId <- withFastStore $ \db -> getGroupChatItemIdByText db user gId contactName_ reportedMessage
processChatCommand vr nm $ APIReportMessage gId reportedItemId reportReason ""
APIUpdateChatItem (ChatRef cType chatId scope) itemId live (UpdatedMessage mc mentions) -> withUser $ \user -> assertAllowedContent mc >> case cType of
CTDirect -> withContactLock "updateChatItem" chatId $ do
unless (null mentions) $ throwCmdError "mentions are not supported in this chat"
ct@Contact {contactId} <- withFastStore $ \db -> getContact db vr user chatId
assertDirectAllowed user MDSnd ct XMsgUpdate_
cci <- withFastStore $ \db -> getDirectCIWithReactions db user ct itemId
case cci of
CChatItem SMDSnd ci@ChatItem {meta = CIMeta {itemSharedMsgId, itemTimed, itemLive, editable}, content = ciContent} -> do
case (ciContent, itemSharedMsgId, editable) of
(CISndMsgContent oldMC, Just itemSharedMId, True) -> do
let changed = mc /= oldMC
if changed || fromMaybe False itemLive
then do
let event = XMsgUpdate itemSharedMId mc M.empty (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive) Nothing
(SndMessage {msgId}, _) <- sendDirectContactMessage user ct event
ci' <- withFastStore' $ \db -> do
currentTs <- liftIO getCurrentTime
when changed $
addInitialAndNewCIVersions db itemId (chatItemTs' ci, oldMC) (currentTs, mc)
let edited = itemLive /= Just True
updateDirectChatItem' db user contactId ci (CISndMsgContent mc) edited live Nothing $ Just msgId
startUpdatedTimedItemThread user (ChatRef CTDirect contactId Nothing) ci ci'
pure $ CRChatItemUpdated user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci')
else pure $ CRChatItemNotChanged user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci)
_ -> throwChatError CEInvalidChatItemUpdate
CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate
CTGroup -> withGroupLock "updateChatItem" chatId $ do
gInfo@GroupInfo {groupId, membership} <- withFastStore $ \db -> getGroupInfo db vr user chatId
when (isNothing scope) $ assertUserGroupRole gInfo GRAuthor
let (_, ft_) = msgContentTexts mc
if prohibitedSimplexLinks gInfo membership ft_
then throwCmdError ("feature not allowed " <> T.unpack (groupFeatureNameText GFSimplexLinks))
else do
-- TODO [knocking] check chat item scope?
cci <- withFastStore $ \db -> getGroupCIWithReactions db user gInfo itemId
case cci of
CChatItem SMDSnd ci@ChatItem {meta = CIMeta {itemSharedMsgId, itemTimed, itemLive, editable}, content = ciContent} -> do
case (ciContent, itemSharedMsgId, editable) of
(CISndMsgContent oldMC, Just itemSharedMId, True) -> do
chatScopeInfo <- mapM (getChatScopeInfo vr user) scope
recipients <- getGroupRecipients vr user gInfo chatScopeInfo groupKnockingVersion
let changed = mc /= oldMC
if changed || fromMaybe False itemLive
then do
ciMentions <- withFastStore $ \db -> getCIMentions db user gInfo ft_ mentions
let msgScope = toMsgScope gInfo <$> chatScopeInfo
mentions' = M.map (\CIMention {memberId} -> MsgMention {memberId}) ciMentions
event = XMsgUpdate itemSharedMId mc mentions' (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive) msgScope
SndMessage {msgId} <- sendGroupMessage user gInfo scope recipients event
ci' <- withFastStore' $ \db -> do
currentTs <- liftIO getCurrentTime
when changed $
addInitialAndNewCIVersions db itemId (chatItemTs' ci, oldMC) (currentTs, mc)
let edited = itemLive /= Just True
ci' <- updateGroupChatItem db user groupId ci (CISndMsgContent mc) edited live $ Just msgId
updateGroupCIMentions db gInfo ci' ciMentions
startUpdatedTimedItemThread user (ChatRef CTGroup groupId scope) ci ci'
pure $ CRChatItemUpdated user (AChatItem SCTGroup SMDSnd (GroupChat gInfo chatScopeInfo) ci')
else pure $ CRChatItemNotChanged user (AChatItem SCTGroup SMDSnd (GroupChat gInfo chatScopeInfo) ci)
_ -> throwChatError CEInvalidChatItemUpdate
CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate
CTLocal -> do
unless (null mentions) $ throwCmdError "mentions are not supported in this chat"
(nf@NoteFolder {noteFolderId}, cci) <- withFastStore $ \db -> (,) <$> getNoteFolder db user chatId <*> getLocalChatItem db user chatId itemId
case cci of
CChatItem SMDSnd ci@ChatItem {content = CISndMsgContent oldMC}
| mc == oldMC -> pure $ CRChatItemNotChanged user (AChatItem SCTLocal SMDSnd (LocalChat nf) ci)
| otherwise -> withFastStore' $ \db -> do
currentTs <- getCurrentTime
addInitialAndNewCIVersions db itemId (chatItemTs' ci, oldMC) (currentTs, mc)
ci' <- updateLocalChatItem' db user noteFolderId ci (CISndMsgContent mc) True
pure $ CRChatItemUpdated user (AChatItem SCTLocal SMDSnd (LocalChat nf) ci')
_ -> throwChatError CEInvalidChatItemUpdate
CTContactRequest -> throwCmdError "not supported"
CTContactConnection -> throwCmdError "not supported"
APIDeleteChatItem (ChatRef cType chatId scope) itemIds mode -> withUser $ \user -> case cType of
CTDirect -> withContactLock "deleteChatItem" chatId $ do
(ct, items) <- getCommandDirectChatItems user chatId itemIds
deletions <- case mode of
CIDMInternal -> deleteDirectCIs user ct items
CIDMInternalMark -> markDirectCIsDeleted user ct items =<< liftIO getCurrentTime
CIDMBroadcast -> do
assertDeletable items
assertDirectAllowed user MDSnd ct XMsgDel_
let msgIds = itemsMsgIds items
events = map (\msgId -> XMsgDel msgId Nothing Nothing) msgIds
forM_ (L.nonEmpty events) $ \events' ->
sendDirectContactMessages user ct events'
if featureAllowed SCFFullDelete forUser ct
then deleteDirectCIs user ct items
else markDirectCIsDeleted user ct items =<< liftIO getCurrentTime
pure $ CRChatItemsDeleted user deletions True False
CTGroup -> withGroupLock "deleteChatItem" chatId $ do
(gInfo, items) <- getCommandGroupChatItems user chatId itemIds
-- TODO [knocking] check scope for all items?
chatScopeInfo <- mapM (getChatScopeInfo vr user) scope
deletions <- case mode of
CIDMInternal -> do
deleteGroupCIs user gInfo chatScopeInfo items Nothing =<< liftIO getCurrentTime
CIDMInternalMark -> do
markGroupCIsDeleted user gInfo chatScopeInfo items Nothing =<< liftIO getCurrentTime
CIDMBroadcast -> do
recipients <- getGroupRecipients vr user gInfo chatScopeInfo groupKnockingVersion
assertDeletable items
assertUserGroupRole gInfo GRObserver -- can still delete messages sent earlier
let msgIds = itemsMsgIds items
events = L.nonEmpty $ map (\msgId -> XMsgDel msgId Nothing $ toMsgScope gInfo <$> chatScopeInfo) msgIds
mapM_ (sendGroupMessages user gInfo Nothing recipients) events
-- TODO delGroupChatItems sends deletion events too. Are they needed?
delGroupChatItems user gInfo chatScopeInfo items False
pure $ CRChatItemsDeleted user deletions True False
CTLocal -> do
(nf, items) <- getCommandLocalChatItems user chatId itemIds
deleteLocalCIs user nf items True False
CTContactRequest -> throwCmdError "not supported"
CTContactConnection -> throwCmdError "not supported"
where
assertDeletable :: forall c. ChatTypeI c => [CChatItem c] -> CM ()
assertDeletable items = do
currentTs <- liftIO getCurrentTime
unless (all (itemDeletable currentTs) items) $ throwChatError CEInvalidChatItemDelete
where
itemDeletable :: UTCTime -> CChatItem c -> Bool
itemDeletable currentTs (CChatItem msgDir ChatItem {meta = CIMeta {itemSharedMsgId, itemTs, itemDeleted}, content}) =
case msgDir of
-- We check with a 6 hour margin compared to CIMeta deletable to account for deletion on the border
SMDSnd -> isJust itemSharedMsgId && deletable' content itemDeleted itemTs (nominalDay + 6 * 3600) currentTs
SMDRcv -> False
itemsMsgIds :: [CChatItem c] -> [SharedMsgId]
itemsMsgIds = mapMaybe (\(CChatItem _ ChatItem {meta = CIMeta {itemSharedMsgId}}) -> itemSharedMsgId)
APIDeleteMemberChatItem gId itemIds -> withUser $ \user -> withGroupLock "deleteChatItem" gId $ do
(gInfo, items) <- getCommandGroupChatItems user gId itemIds
-- TODO [knocking] check scope is Nothing for all items? (prohibit moderation in support chats?)
ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo
let recipients = filter memberCurrent ms
deletions <- delGroupChatItemsForMembers user gInfo Nothing recipients items
pure $ CRChatItemsDeleted user deletions True False
APIArchiveReceivedReports gId -> withUser $ \user -> withFastStore $ \db -> do
g <- getGroupInfo db vr user gId
deleteTs <- liftIO getCurrentTime
ciIds <- liftIO $ markReceivedGroupReportsDeleted db user g deleteTs
pure $ CRGroupChatItemsDeleted user g ciIds True (Just $ membership g)
APIDeleteReceivedReports gId itemIds mode -> withUser $ \user -> withGroupLock "deleteReports" gId $ do
(gInfo, items) <- getCommandGroupChatItems user gId itemIds
unless (all isRcvReport items) $ throwCmdError "some items are not received reports"
-- TODO [knocking] scope can be different for each item if reports are from different members
-- TODO (currently we pass Nothing as scope which is wrong)
deletions <- case mode of
CIDMInternal -> deleteGroupCIs user gInfo Nothing items Nothing =<< liftIO getCurrentTime
CIDMInternalMark -> markGroupCIsDeleted user gInfo Nothing items Nothing =<< liftIO getCurrentTime
CIDMBroadcast -> do
ms <- withFastStore' $ \db -> getGroupModerators db vr user gInfo
let recipients = filter memberCurrent ms
delGroupChatItemsForMembers user gInfo Nothing recipients items
pure $ CRChatItemsDeleted user deletions True False
where
isRcvReport = \case
CChatItem _ ChatItem {content = CIRcvMsgContent (MCReport {})} -> True
_ -> False
APIChatItemReaction (ChatRef cType chatId scope) itemId add reaction -> withUser $ \user -> case cType of
CTDirect ->
withContactLock "chatItemReaction" chatId $
withFastStore (\db -> (,) <$> getContact db vr user chatId <*> getDirectChatItem db user chatId itemId) >>= \case
(ct, CChatItem md ci@ChatItem {meta = CIMeta {itemSharedMsgId = Just itemSharedMId}}) -> do
unless (featureAllowed SCFReactions forUser ct) $
throwCmdError $ "feature not allowed " <> T.unpack (chatFeatureNameText CFReactions)
unless (ciReactionAllowed ci) $
throwCmdError "reaction not allowed - chat item has no content"
rs <- withFastStore' $ \db -> getDirectReactions db ct itemSharedMId True
checkReactionAllowed rs
(SndMessage {msgId}, _) <- sendDirectContactMessage user ct $ XMsgReact itemSharedMId Nothing Nothing reaction add
createdAt <- liftIO getCurrentTime
reactions <- withFastStore' $ \db -> do
setDirectReaction db ct itemSharedMId True reaction add msgId createdAt
liftIO $ getDirectCIReactions db ct itemSharedMId
let ci' = CChatItem md ci {reactions}
r = ACIReaction SCTDirect SMDSnd (DirectChat ct) $ CIReaction CIDirectSnd ci' createdAt reaction
pure $ CRChatItemReaction user add r
_ -> throwCmdError "reaction not possible - no shared item ID"
CTGroup ->
withGroupLock "chatItemReaction" chatId $ do
-- TODO [knocking] check chat item scope?
(g@GroupInfo {membership}, CChatItem md ci) <- withFastStore $ \db -> do
g <- getGroupInfo db vr user chatId
(g,) <$> getGroupCIWithReactions db user g itemId
chatScopeInfo <- mapM (getChatScopeInfo vr user) scope
recipients <- getGroupRecipients vr user g chatScopeInfo groupKnockingVersion
case ci of
ChatItem {meta = CIMeta {itemSharedMsgId = Just itemSharedMId}} -> do
unless (groupFeatureAllowed SGFReactions g) $
throwCmdError $ "feature not allowed " <> T.unpack (chatFeatureNameText CFReactions)
unless (ciReactionAllowed ci) $
throwCmdError "reaction not allowed - chat item has no content"
let GroupMember {memberId = itemMemberId} = chatItemMember g ci
rs <- withFastStore' $ \db -> getGroupReactions db g membership itemMemberId itemSharedMId True
checkReactionAllowed rs
SndMessage {msgId} <- sendGroupMessage user g scope recipients (XMsgReact itemSharedMId (Just itemMemberId) (toMsgScope g <$> chatScopeInfo) reaction add)
createdAt <- liftIO getCurrentTime
reactions <- withFastStore' $ \db -> do
setGroupReaction db g membership itemMemberId itemSharedMId True reaction add msgId createdAt
liftIO $ getGroupCIReactions db g itemMemberId itemSharedMId
let ci' = CChatItem md ci {reactions}
r = ACIReaction SCTGroup SMDSnd (GroupChat g chatScopeInfo) $ CIReaction CIGroupSnd ci' createdAt reaction
pure $ CRChatItemReaction user add r
_ -> throwCmdError "invalid reaction"
CTLocal -> throwCmdError "not supported"
CTContactRequest -> throwCmdError "not supported"
CTContactConnection -> throwCmdError "not supported"
where
checkReactionAllowed rs = do
when ((reaction `elem` rs) == add) $
throwCmdError $ "reaction already " <> if add then "added" else "removed"
when (add && length rs >= maxMsgReactions) $
throwCmdError "too many reactions"
APIGetReactionMembers userId groupId itemId reaction -> withUserId userId $ \user -> do
memberReactions <- withStore $ \db -> do
CChatItem _ ChatItem {meta = CIMeta {itemSharedMsgId = Just itemSharedMId}} <- getGroupChatItem db user groupId itemId
liftIO $ getReactionMembers db vr user groupId itemSharedMId reaction
pure $ CRReactionMembers user memberReactions
-- TODO [knocking] forward from scope?
APIPlanForwardChatItems (ChatRef fromCType fromChatId _scope) itemIds -> withUser $ \user -> case fromCType of
CTDirect -> planForward user . snd =<< getCommandDirectChatItems user fromChatId itemIds
CTGroup -> planForward user . snd =<< getCommandGroupChatItems user fromChatId itemIds
CTLocal -> planForward user . snd =<< getCommandLocalChatItems user fromChatId itemIds
CTContactRequest -> throwCmdError "not supported"
CTContactConnection -> throwCmdError "not supported"
where
planForward :: User -> [CChatItem c] -> CM ChatResponse
planForward user items = do
(itemIds', forwardErrors) <- unzip <$> mapM planItemForward items
let forwardConfirmation = case catMaybes forwardErrors of
[] -> Nothing
errs -> Just $ case mainErr of
FFENotAccepted _ -> FCFilesNotAccepted fileIds
FFEInProgress -> FCFilesInProgress filesCount
FFEMissing -> FCFilesMissing filesCount
FFEFailed -> FCFilesFailed filesCount
where
mainErr = minimum errs
fileIds = catMaybes $ map (\case FFENotAccepted ftId -> Just ftId; _ -> Nothing) errs
filesCount = length $ filter (mainErr ==) errs
pure CRForwardPlan {user, itemsCount = length itemIds, chatItemIds = catMaybes itemIds', forwardConfirmation}
where
planItemForward :: CChatItem c -> CM (Maybe ChatItemId, Maybe ForwardFileError)
planItemForward (CChatItem _ ci) = forwardMsgContent ci >>= maybe (pure (Nothing, Nothing)) (forwardContentPlan ci)
forwardContentPlan :: ChatItem c d -> MsgContent -> CM (Maybe ChatItemId, Maybe ForwardFileError)
forwardContentPlan ChatItem {file, meta = CIMeta {itemId}} mc = case file of
Nothing -> pure (Just itemId, Nothing)
Just CIFile {fileId, fileStatus, fileSource} -> case ciFileForwardError fileId fileStatus of
Just err -> pure $ itemIdWithoutFile err
Nothing -> case fileSource of
Just CryptoFile {filePath} -> do
exists <- doesFileExist =<< lift (toFSFilePath filePath)
pure $ if exists then (Just itemId, Nothing) else itemIdWithoutFile FFEMissing
Nothing -> pure $ itemIdWithoutFile FFEMissing
where
itemIdWithoutFile err = (if hasContent then Just itemId else Nothing, Just err)
hasContent = case mc of
MCText _ -> True
MCLink {} -> True
MCImage {} -> True
MCVideo {text} -> text /= ""
MCVoice {text} -> text /= ""
MCFile t -> t /= ""
MCReport {} -> True
MCChat {} -> True
MCUnknown {} -> True
-- TODO [knocking] forward from / to scope
APIForwardChatItems toChat@(ChatRef toCType toChatId toScope) fromChat@(ChatRef fromCType fromChatId _fromScope) itemIds itemTTL -> withUser $ \user -> case toCType of
CTDirect -> do
cmrs <- prepareForward user
case L.nonEmpty cmrs of
Just cmrs' ->
withContactLock "forwardChatItem, to contact" toChatId $
sendContactContentMessages user toChatId False itemTTL cmrs'
Nothing -> pure $ CRNewChatItems user []
CTGroup -> do
cmrs <- prepareForward user
case L.nonEmpty cmrs of
Just cmrs' ->
withGroupLock "forwardChatItem, to group" toChatId $ do
gInfo <- withFastStore $ \db -> getGroupInfo db vr user toChatId
sendGroupContentMessages user gInfo toScope False itemTTL cmrs'
Nothing -> pure $ CRNewChatItems user []
CTLocal -> do
cmrs <- prepareForward user
case L.nonEmpty cmrs of
Just cmrs' ->
createNoteFolderContentItems user toChatId cmrs'
Nothing -> pure $ CRNewChatItems user []
CTContactRequest -> throwCmdError "not supported"
CTContactConnection -> throwCmdError "not supported"
where
prepareForward :: User -> CM [ComposedMessageReq]
prepareForward user = case fromCType of
CTDirect -> withContactLock "forwardChatItem, from contact" fromChatId $ do
(ct, items) <- getCommandDirectChatItems user fromChatId itemIds
catMaybes <$> mapM (\ci -> ciComposeMsgReq ct ci <$$> prepareMsgReq ci) items
where
ciComposeMsgReq :: Contact -> CChatItem 'CTDirect -> (MsgContent, Maybe CryptoFile) -> ComposedMessageReq
ciComposeMsgReq ct (CChatItem md ci) (mc', file) =
let itemId = chatItemId' ci
ciff = forwardCIFF ci $ Just (CIFFContact (forwardName ct) (toMsgDirection md) (Just fromChatId) (Just itemId))
in (composedMessage file mc', ciff, msgContentTexts mc', M.empty)
where
forwardName :: Contact -> ContactName
forwardName Contact {profile = LocalProfile {displayName, localAlias}}
| localAlias /= "" = localAlias
| otherwise = displayName
-- TODO [knocking] from scope?
CTGroup -> withGroupLock "forwardChatItem, from group" fromChatId $ do
(gInfo, items) <- getCommandGroupChatItems user fromChatId itemIds
catMaybes <$> mapM (\ci -> ciComposeMsgReq gInfo ci <$$> prepareMsgReq ci) items
where
ciComposeMsgReq :: GroupInfo -> CChatItem 'CTGroup -> (MsgContent, Maybe CryptoFile) -> ComposedMessageReq
ciComposeMsgReq gInfo (CChatItem md ci@ChatItem {mentions, formattedText}) (mc, file) = do
let itemId = chatItemId' ci
ciff = forwardCIFF ci $ Just (CIFFGroup (forwardName gInfo) (toMsgDirection md) (Just fromChatId) (Just itemId))
-- updates text to reflect current mentioned member names
(mc', _, mentions') = updatedMentionNames mc formattedText mentions
-- only includes mentions when forwarding to the same group
ciMentions = if toChat == fromChat then mentions' else M.empty
-- no need to have mentions in ComposedMessage, they are in ciMentions
in (ComposedMessage file Nothing mc' M.empty, ciff, msgContentTexts mc', ciMentions)
where
forwardName :: GroupInfo -> ContactName
forwardName GroupInfo {groupProfile = GroupProfile {displayName}} = displayName
CTLocal -> do
(_, items) <- getCommandLocalChatItems user fromChatId itemIds
catMaybes <$> mapM (\ci -> ciComposeMsgReq ci <$$> prepareMsgReq ci) items
where
ciComposeMsgReq :: CChatItem 'CTLocal -> (MsgContent, Maybe CryptoFile) -> ComposedMessageReq
ciComposeMsgReq (CChatItem _ ci) (mc', file) =
let ciff = forwardCIFF ci Nothing
in (composedMessage file mc', ciff, msgContentTexts mc', M.empty)
CTContactRequest -> throwCmdError "not supported"
CTContactConnection -> throwCmdError "not supported"
where
prepareMsgReq :: CChatItem c -> CM (Maybe (MsgContent, Maybe CryptoFile))
prepareMsgReq (CChatItem _ ci) = forwardMsgContent ci $>>= forwardContent ci
forwardCIFF :: ChatItem c d -> Maybe CIForwardedFrom -> Maybe CIForwardedFrom
forwardCIFF ChatItem {meta = CIMeta {itemForwarded}} ciff = case itemForwarded of
Nothing -> ciff
Just CIFFUnknown -> ciff
Just prevCIFF -> Just prevCIFF
forwardContent :: ChatItem c d -> MsgContent -> CM (Maybe (MsgContent, Maybe CryptoFile))
forwardContent ChatItem {file} mc = case file of
Nothing -> pure $ Just (mc, Nothing)
Just CIFile {fileName, fileStatus, fileSource = Just fromCF@CryptoFile {filePath}}
| ciFileLoaded fileStatus ->
chatReadVar filesFolder >>= \case
Nothing ->
ifM (doesFileExist filePath) (pure $ Just (mc, Just fromCF)) (pure contentWithoutFile)
Just filesFolder -> do
let fsFromPath = filesFolder </> filePath
ifM
(doesFileExist fsFromPath)
( do
newFileName <- liftIO $ maybe (pure fileName) (generateNewFileName fileName) $ mediaFilePrefix mc
fsNewPath <- liftIO $ filesFolder `uniqueCombine` newFileName
liftIO $ B.writeFile fsNewPath "" -- create empty file
encrypt <- chatReadVar encryptLocalFiles
cfArgs <- if encrypt then Just <$> (atomically . CF.randomArgs =<< asks random) else pure Nothing
let toCF = CryptoFile fsNewPath cfArgs
-- to keep forwarded file in case original is deleted
liftIOEither $ runExceptT $ withExceptT (ChatError . CEInternalError . show) $ copyCryptoFile (fromCF {filePath = fsFromPath} :: CryptoFile) toCF
pure $ Just (mc, Just (toCF {filePath = takeFileName fsNewPath} :: CryptoFile))
)
(pure contentWithoutFile)
_ -> pure contentWithoutFile
where
contentWithoutFile = case mc of
MCImage {} -> Just (mc, Nothing)
MCLink {} -> Just (mc, Nothing)
_ | contentText /= "" -> Just (MCText contentText, Nothing)
_ -> Nothing
contentText = msgContentText mc
copyCryptoFile :: CryptoFile -> CryptoFile -> ExceptT CF.FTCryptoError IO ()
copyCryptoFile fromCF@CryptoFile {filePath = fsFromPath, cryptoArgs = fromArgs} toCF@CryptoFile {cryptoArgs = toArgs} = do
fromSizeFull <- getFileSize fsFromPath
let fromSize = fromSizeFull - maybe 0 (const $ toInteger C.authTagSize) fromArgs
CF.withFile fromCF ReadMode $ \fromH ->
CF.withFile toCF WriteMode $ \toH -> do
copyChunks fromH toH fromSize
forM_ fromArgs $ \_ -> CF.hGetTag fromH
forM_ toArgs $ \_ -> liftIO $ CF.hPutTag toH
where
copyChunks :: CF.CryptoFileHandle -> CF.CryptoFileHandle -> Integer -> ExceptT CF.FTCryptoError IO ()
copyChunks r w size = do
let chSize = min size U.chunkSize
chSize' = fromIntegral chSize
size' = size - chSize
ch <- liftIO $ CF.hGet r chSize'
when (B.length ch /= chSize') $ throwError $ CF.FTCEFileIOError "encrypting file: unexpected EOF"
liftIO . CF.hPut w $ LB.fromStrict ch
when (size' > 0) $ copyChunks r w size'
mediaFilePrefix :: MsgContent -> Maybe FilePath
mediaFilePrefix = \case
MCImage {} -> Just imageFilePrefix
MCVoice {} -> Just voiceFilePrefix
MCVideo {} -> Just videoFilePrefix
_ -> Nothing
generateNewFileName fileName prefix = do
currentDate <- liftIO getCurrentTime
let formattedDate = formatTime defaultTimeLocale "%Y%m%d_%H%M%S" currentDate
let ext = takeExtension fileName
pure $ prefix <> formattedDate <> ext
APIUserRead userId -> withUserId userId $ \user -> withFastStore' (`setUserChatsRead` user) >> ok user
UserRead -> withUser $ \User {userId} -> processChatCommand vr nm $ APIUserRead userId
APIChatRead chatRef@(ChatRef cType chatId scope_) -> withUser $ \_ -> case cType of
CTDirect -> do
user <- withFastStore $ \db -> getUserByContactId db chatId
ts <- liftIO getCurrentTime
timedItems <- withFastStore' $ \db -> do
timedItems <- getDirectUnreadTimedItems db user chatId
updateDirectChatItemsRead db user chatId
setDirectChatItemsDeleteAt db user chatId timedItems ts
forM_ timedItems $ \(itemId, deleteAt) -> startProximateTimedItemThread user (chatRef, itemId) deleteAt
ok user
CTGroup -> do
(user, gInfo) <- withFastStore $ \db -> do
user <- getUserByGroupId db chatId
gInfo <- getGroupInfo db vr user chatId
pure (user, gInfo)
ts <- liftIO getCurrentTime
case scope_ of
Nothing -> do
timedItems <- withFastStore' $ \db -> do
timedItems <- getGroupUnreadTimedItems db user chatId Nothing
updateGroupChatItemsRead db user gInfo
setGroupChatItemsDeleteAt db user chatId timedItems ts
forM_ timedItems $ \(itemId, deleteAt) -> startProximateTimedItemThread user (chatRef, itemId) deleteAt
ok user
Just scope -> do
scopeInfo <- getChatScopeInfo vr user scope
(gInfo', m', timedItems) <- withFastStore' $ \db -> do
timedItems <- getGroupUnreadTimedItems db user chatId (Just scope)
(gInfo', m') <- updateSupportChatItemsRead db vr user gInfo scopeInfo
timedItems' <- setGroupChatItemsDeleteAt db user chatId timedItems ts
pure (gInfo', m', timedItems')
forM_ timedItems $ \(itemId, deleteAt) -> startProximateTimedItemThread user (chatRef, itemId) deleteAt
pure $ CRMemberSupportChatRead user gInfo' m'
CTLocal -> do
user <- withFastStore $ \db -> getUserByNoteFolderId db chatId
withFastStore' $ \db -> updateLocalChatItemsRead db user chatId
ok user
CTContactRequest -> throwCmdError "not supported"
CTContactConnection -> throwCmdError "not supported"
APIChatItemsRead chatRef@(ChatRef cType chatId scope) itemIds -> withUser $ \_ -> case cType of
CTDirect -> do
(user, ct) <- withFastStore $ \db -> do
user <- getUserByContactId db chatId
ct <- getContact db vr user chatId
pure (user, ct)
timedItems <- withFastStore' $ \db -> do
timedItems <- updateDirectChatItemsReadList db user chatId itemIds
setDirectChatItemsDeleteAt db user chatId timedItems =<< getCurrentTime
forM_ timedItems $ \(itemId, deleteAt) -> startProximateTimedItemThread user (chatRef, itemId) deleteAt
pure $ CRItemsReadForChat user (AChatInfo SCTDirect $ DirectChat ct)
CTGroup -> do
(user, gInfo) <- withFastStore $ \db -> do
user <- getUserByGroupId db chatId
gInfo <- getGroupInfo db vr user chatId
pure (user, gInfo)
chatScopeInfo <- mapM (getChatScopeInfo vr user) scope
(timedItems, gInfo') <- withFastStore $ \db -> do
(timedItems, gInfo') <- updateGroupChatItemsReadList db vr user gInfo chatScopeInfo itemIds
timedItems' <- liftIO $ setGroupChatItemsDeleteAt db user chatId timedItems =<< getCurrentTime
pure (timedItems', gInfo')
forM_ timedItems $ \(itemId, deleteAt) -> startProximateTimedItemThread user (chatRef, itemId) deleteAt
pure $ CRItemsReadForChat user (AChatInfo SCTGroup $ GroupChat gInfo' Nothing)
CTLocal -> throwCmdError "not supported"
CTContactRequest -> throwCmdError "not supported"
CTContactConnection -> throwCmdError "not supported"
APIChatUnread (ChatRef cType chatId scope) unreadChat -> withUser $ \user -> case cType of
CTDirect -> do
withFastStore $ \db -> do
ct <- getContact db vr user chatId
liftIO $ updateContactUnreadChat db user ct unreadChat
ok user
-- TODO [knocking] set support chat as unread?
CTGroup | isNothing scope -> do
withFastStore $ \db -> do
gInfo <- getGroupInfo db vr user chatId
liftIO $ updateGroupUnreadChat db user gInfo unreadChat
ok user
CTLocal -> do
withFastStore $ \db -> do
nf <- getNoteFolder db user chatId
liftIO $ updateNoteFolderUnreadChat db user nf unreadChat
ok user
_ -> throwCmdError "not supported"
APIDeleteChat cRef@(ChatRef cType chatId scope) cdm -> withUser $ \user@User {userId} -> case cType of
CTDirect -> do
ct <- withFastStore $ \db -> getContact db vr user chatId
filesInfo <- withFastStore' $ \db -> getContactFileInfo db user ct
withContactLock "deleteChat direct" chatId $
case cdm of
CDMFull notify -> do
deleteCIFiles user filesInfo
sendDelDeleteConns ct notify
-- functions below are called in separate transactions to prevent crashes on android
-- (possibly, race condition on integrity check?)
withFastStore' $ \db -> do
deleteContactConnections db user ct
deleteContactFiles db user ct
withFastStore $ \db -> deleteContact db user ct
pure $ CRContactDeleted user ct
CDMEntity notify -> do
cancelFilesInProgress user filesInfo
sendDelDeleteConns ct notify
ct' <- withFastStore $ \db -> do
liftIO $ deleteContactConnections db user ct
liftIO $ void $ updateContactStatus db user ct CSDeletedByUser
getContact db vr user chatId
pure $ CRContactDeleted user ct'
CDMMessages -> do
void $ processChatCommand vr nm $ APIClearChat cRef
withFastStore' $ \db -> setContactChatDeleted db user ct True
pure $ CRContactDeleted user ct {chatDeleted = True}
where
sendDelDeleteConns ct notify = do
let doSendDel = contactReady ct && contactActive ct && notify
when doSendDel $ void (sendDirectContactMessage user ct XDirectDel) `catchAllErrors` const (pure ())
contactConnIds <- map aConnId <$> withFastStore' (\db -> getContactConnections db vr userId ct)
deleteAgentConnectionsAsync' contactConnIds doSendDel
CTContactConnection -> withConnectionLock "deleteChat contactConnection" chatId $ do
conn@PendingContactConnection {pccAgentConnId = AgentConnId acId} <- withFastStore $ \db -> getPendingContactConnection db userId chatId
deleteAgentConnectionAsync acId
withFastStore' $ \db -> deletePendingContactConnection db userId chatId
pure $ CRContactConnectionDeleted user conn
CTGroup | isNothing scope -> do
gInfo@GroupInfo {membership} <- withFastStore $ \db -> getGroupInfo db vr user chatId
let GroupMember {memberRole = membershipMemRole} = membership
let isOwner = membershipMemRole == GROwner
canDelete = isOwner || not (memberCurrent membership)
unless canDelete $ throwChatError $ CEGroupUserRole gInfo GROwner
filesInfo <- withFastStore' $ \db -> getGroupFileInfo db user gInfo
withGroupLock "deleteChat group" chatId $ do
deleteCIFiles user filesInfo
(members, recipients) <- getRecipients gInfo
let doSendDel = memberActive membership && isOwner
when doSendDel . void $ sendGroupMessage' user gInfo recipients XGrpDel
deleteGroupLinkIfExists user gInfo
deleteMembersConnections' user members doSendDel
updateCIGroupInvitationStatus user gInfo CIGISRejected `catchAllErrors` \_ -> pure ()
withFastStore' $ \db -> deleteGroupChatItems db user gInfo
withFastStore' $ \db -> cleanupHostGroupLinkConn db user gInfo
withFastStore' $ \db -> deleteGroupMembers db user gInfo
withFastStore' $ \db -> deleteGroup db user gInfo
pure $ CRGroupDeletedUser user gInfo
where
getRecipients gInfo@GroupInfo {useRelays}
| isTrue useRelays = do
relays <- withFastStore' $ \db -> getGroupRelays db vr user gInfo
pure (relays, relays)
| otherwise = do
ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo
pure (ms, filter memberCurrentOrPending ms)
_ -> throwCmdError "not supported"
APIClearChat (ChatRef cType chatId scope) -> withUser $ \user@User {userId} -> case cType of
CTDirect -> do
ct <- withFastStore $ \db -> getContact db vr user chatId
filesInfo <- withFastStore' $ \db -> getContactFileInfo db user ct
deleteCIFiles user filesInfo
withFastStore' $ \db -> deleteContactCIs db user ct
pure $ CRChatCleared user (AChatInfo SCTDirect $ DirectChat ct)
CTGroup | isNothing scope -> do
gInfo <- withFastStore $ \db -> getGroupInfo db vr user chatId
filesInfo <- withFastStore' $ \db -> getGroupFileInfo db user gInfo
deleteCIFiles user filesInfo
withFastStore' $ \db -> deleteGroupChatItemsMessages db user gInfo
membersToDelete <- withFastStore' $ \db -> getGroupMembersForExpiration db vr user gInfo
forM_ membersToDelete $ \m -> withFastStore' $ \db -> deleteGroupMember db user m
pure $ CRChatCleared user (AChatInfo SCTGroup $ GroupChat gInfo Nothing)
CTLocal -> do
nf <- withFastStore $ \db -> getNoteFolder db user chatId
filesInfo <- withFastStore' $ \db -> getNoteFolderFileInfo db user nf
deleteFilesLocally filesInfo
withFastStore' $ \db -> deleteNoteFolderFiles db userId nf
withFastStore' $ \db -> deleteNoteFolderCIs db user nf
pure $ CRChatCleared user (AChatInfo SCTLocal $ LocalChat nf)
_ -> throwCmdError "not supported"
APIAcceptContact incognito connReqId -> withUser $ \user@User {userId} -> do
uclData_ <- withFastStore $ \db -> do
uclId_ <- getUserContactLinkIdByCReq db connReqId
forM uclId_ $ \uclId -> do -- address may be deleted
uclGLinkInfo <- getUserContactLinkById db userId uclId
pure (uclId, uclGLinkInfo)
withContactRequestLock "acceptContact" connReqId $ case uclData_ of
Nothing -> do -- address was deleted
when incognito $ throwCmdError "incognito not allowed when address is not found"
cReq <- withFastStore $ \db -> getContactRequest db user connReqId
(ct, _sqSecured) <- acceptCReq user cReq True
pure $ CRAcceptingContactRequest user ct
Just (uclId, (ucl@UserContactLink {shortLinkDataSet}, gLinkInfo_)) -> do
when (shortLinkDataSet && incognito) $ throwCmdError "incognito not allowed for address with short link data"
withUserContactLock "acceptContact" uclId $ do
cReq <- withFastStore $ \db -> getContactRequest db user connReqId
let contactUsed = isNothing gLinkInfo_ -- for redundancy, as group link requests are auto-accepted
(ct, sqSecured) <- acceptCReq user cReq contactUsed
when sqSecured $ sendWelcomeMsg user ct ucl cReq
pure $ CRAcceptingContactRequest user ct
where
acceptCReq user cReq contactUsed = do
(ct, conn, sqSecured) <- acceptContactRequest nm user cReq incognito
ct' <- withStore' $ \db -> do
updateContactAccepted db user ct contactUsed
conn' <-
if sqSecured
then updateConnectionStatusFromTo db conn ConnNew ConnSndReady
else pure conn
pure ct {contactUsed, activeConn = Just conn'}
pure (ct', sqSecured)
sendWelcomeMsg user ct ucl UserContactRequest {welcomeSharedMsgId} =
forM_ (autoReply $ addressSettings ucl) $ \mc -> case welcomeSharedMsgId of
Just smId ->
void $ sendDirectContactMessage user ct $ XMsgUpdate smId mc M.empty Nothing Nothing Nothing
Nothing -> do
(msg, _) <- sendDirectContactMessage user ct $ XMsgNew $ MCSimple $ extMsgContent mc Nothing
ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc)
toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci]
APIRejectContact connReqId -> withUser $ \user -> do
uclId_ <- withFastStore $ \db -> getUserContactLinkIdByCReq db connReqId
withContactRequestLock "rejectContact" connReqId $ case uclId_ of
Nothing -> rejectCReq user -- address was deleted
Just uclId -> withUserContactLock "rejectContact" uclId $ rejectCReq user
where
rejectCReq user = do
(cReq@UserContactRequest {agentInvitationId = AgentInvId invId}, ct_) <-
withFastStore $ \db -> do
cReq@UserContactRequest {contactId_} <- getContactRequest db user connReqId
ct_ <- forM contactId_ $ \contactId -> do
ct <- getContact db vr user contactId
deleteContact db user ct
pure ct
liftIO $ deleteContactRequest db user connReqId
pure (cReq, ct_)
withAgent (`rejectContact` invId)
pure $ CRContactRequestRejected user cReq ct_
APISendCallInvitation contactId callType -> withUser $ \user -> do
-- party initiating call
ct <- withFastStore $ \db -> getContact db vr user contactId
assertDirectAllowed user MDSnd ct XCallInv_
if featureAllowed SCFCalls forUser ct
then do
calls <- asks currentCalls
withContactLock "sendCallInvitation" contactId $ do
g <- asks random
callId <- atomically $ CallId <$> C.randomBytes 16 g
callUUID <- UUID.toText <$> liftIO V4.nextRandom
dhKeyPair <- atomically $ if encryptedCall callType then Just <$> C.generateKeyPair g else pure Nothing
let invitation = CallInvitation {callType, callDhPubKey = fst <$> dhKeyPair}
callState = CallInvitationSent {localCallType = callType, localDhPrivKey = snd <$> dhKeyPair}
(msg, _) <- sendDirectContactMessage user ct (XCallInv callId invitation)
ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndCall CISCallPending 0)
let call' = Call {contactId, callId, callUUID, chatItemId = chatItemId' ci, callState, callTs = chatItemTs' ci}
call_ <- atomically $ TM.lookupInsert contactId call' calls
forM_ call_ $ \call -> updateCallItemStatus user ct call WCSDisconnected Nothing
toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci]
ok user
else throwCmdError $ "feature not allowed " <> T.unpack (chatFeatureNameText CFCalls)
SendCallInvitation cName callType -> withUser $ \user -> do
contactId <- withFastStore $ \db -> getContactIdByName db user cName
processChatCommand vr nm $ APISendCallInvitation contactId callType
APIRejectCall contactId ->
-- party accepting call
withCurrentCall contactId $ \user ct Call {chatItemId, callState} -> case callState of
CallInvitationReceived {} -> do
let aciContent = ACIContent SMDRcv $ CIRcvCall CISCallRejected 0
withFastStore' $ \db -> setDirectChatItemRead db user contactId chatItemId
timed_ <- contactCITimed ct
updateDirectChatItemView user ct chatItemId aciContent False False timed_ Nothing
forM_ (timed_ >>= timedDeleteAt') $
startProximateTimedItemThread user (ChatRef CTDirect contactId Nothing, chatItemId)
pure Nothing
_ -> throwChatError . CECallState $ callStateTag callState
APISendCallOffer contactId WebRTCCallOffer {callType, rtcSession} ->
-- party accepting call
withCurrentCall contactId $ \user ct call@Call {callId, chatItemId, callState} -> case callState of
CallInvitationReceived {peerCallType, localDhPubKey, sharedKey} -> do
let callDhPubKey = if encryptedCall callType then localDhPubKey else Nothing
offer = CallOffer {callType, rtcSession, callDhPubKey}
callState' = CallOfferSent {localCallType = callType, peerCallType, localCallSession = rtcSession, sharedKey}
aciContent = ACIContent SMDRcv $ CIRcvCall CISCallAccepted 0
(SndMessage {msgId}, _) <- sendDirectContactMessage user ct (XCallOffer callId offer)
withFastStore' $ \db -> setDirectChatItemRead db user contactId chatItemId
updateDirectChatItemView user ct chatItemId aciContent False False Nothing $ Just msgId
pure $ Just call {callState = callState'}
_ -> throwChatError . CECallState $ callStateTag callState
APISendCallAnswer contactId rtcSession ->
-- party initiating call
withCurrentCall contactId $ \user ct call@Call {callId, chatItemId, callState} -> case callState of
CallOfferReceived {localCallType, peerCallType, peerCallSession, sharedKey} -> do
let callState' = CallNegotiated {localCallType, peerCallType, localCallSession = rtcSession, peerCallSession, sharedKey}
aciContent = ACIContent SMDSnd $ CISndCall CISCallNegotiated 0
(SndMessage {msgId}, _) <- sendDirectContactMessage user ct (XCallAnswer callId CallAnswer {rtcSession})
updateDirectChatItemView user ct chatItemId aciContent False False Nothing $ Just msgId
pure $ Just call {callState = callState'}
_ -> throwChatError . CECallState $ callStateTag callState
APISendCallExtraInfo contactId rtcExtraInfo ->
-- any call party
withCurrentCall contactId $ \user ct call@Call {callId, callState} -> case callState of
CallOfferSent {localCallType, peerCallType, localCallSession, sharedKey} -> do
-- TODO update the list of ice servers in localCallSession
void . sendDirectContactMessage user ct $ XCallExtra callId CallExtraInfo {rtcExtraInfo}
let callState' = CallOfferSent {localCallType, peerCallType, localCallSession, sharedKey}
pure $ Just call {callState = callState'}
CallNegotiated {localCallType, peerCallType, localCallSession, peerCallSession, sharedKey} -> do
-- TODO update the list of ice servers in localCallSession
void . sendDirectContactMessage user ct $ XCallExtra callId CallExtraInfo {rtcExtraInfo}
let callState' = CallNegotiated {localCallType, peerCallType, localCallSession, peerCallSession, sharedKey}
pure $ Just call {callState = callState'}
_ -> throwChatError . CECallState $ callStateTag callState
APIEndCall contactId ->
-- any call party
withCurrentCall contactId $ \user ct call@Call {callId} -> do
(SndMessage {msgId}, _) <- sendDirectContactMessage user ct (XCallEnd callId)
updateCallItemStatus user ct call WCSDisconnected $ Just msgId
pure Nothing
APIGetCallInvitations -> withUser' $ \_ -> lift $ do
calls <- asks currentCalls >>= readTVarIO
let invs = mapMaybe callInvitation $ M.elems calls
rcvCallInvitations <- rights <$> mapM rcvCallInvitation invs
pure $ CRCallInvitations rcvCallInvitations
where
callInvitation Call {contactId, callUUID, callState, callTs} = case callState of
CallInvitationReceived {peerCallType, sharedKey} -> Just (contactId, callUUID, callTs, peerCallType, sharedKey)
_ -> Nothing
rcvCallInvitation (contactId, callUUID, callTs, peerCallType, sharedKey) = runExceptT . withFastStore $ \db -> do
user <- getUserByContactId db contactId
contact <- getContact db vr user contactId
pure RcvCallInvitation {user, contact, callType = peerCallType, sharedKey, callUUID, callTs}
APICallStatus contactId receivedStatus ->
withCurrentCall contactId $ \user ct call ->
updateCallItemStatus user ct call receivedStatus Nothing $> Just call
APIUpdateProfile userId profile -> withUserId userId (`updateProfile` profile)
APISetContactPrefs contactId prefs' -> withUser $ \user -> do
ct <- withFastStore $ \db -> getContact db vr user contactId
updateContactPrefs user ct prefs'
APISetContactAlias contactId localAlias -> withUser $ \user@User {userId} -> do
ct' <- withFastStore $ \db -> do
ct <- getContact db vr user contactId
liftIO $ updateContactAlias db userId ct localAlias
pure $ CRContactAliasUpdated user ct'
APISetGroupAlias gId localAlias -> withUser $ \user@User {userId} -> do
gInfo' <- withFastStore $ \db -> do
gInfo <- getGroupInfo db vr user gId
liftIO $ updateGroupAlias db userId gInfo localAlias
pure $ CRGroupAliasUpdated user gInfo'
APISetConnectionAlias connId localAlias -> withUser $ \user@User {userId} -> do
conn' <- withFastStore $ \db -> do
conn <- getPendingContactConnection db userId connId
liftIO $ updateContactConnectionAlias db userId conn localAlias
pure $ CRConnectionAliasUpdated user conn'
APISetUserUIThemes uId uiThemes -> withUser $ \user@User {userId} -> do
user'@User {userId = uId'} <- withFastStore $ \db -> do
user' <- getUser db uId
liftIO $ setUserUIThemes db user uiThemes
pure user'
when (userId == uId') $ chatWriteVar currentUser $ Just (user :: User) {uiThemes}
ok user'
APISetChatUIThemes (ChatRef cType chatId scope) uiThemes -> withUser $ \user -> case cType of
CTDirect -> do
withFastStore $ \db -> do
ct <- getContact db vr user chatId
liftIO $ setContactUIThemes db user ct uiThemes
ok user
CTGroup | isNothing scope -> do
withFastStore $ \db -> do
g <- getGroupInfo db vr user chatId
liftIO $ setGroupUIThemes db user g uiThemes
ok user
_ -> throwCmdError "not supported"
APIGetNtfToken -> withUser' $ \_ -> crNtfToken <$> withAgent getNtfToken
APIRegisterToken token mode -> withUser $ \_ ->
CRNtfTokenStatus <$> withAgent (\a -> registerNtfToken a nm token mode)
APIVerifyToken token nonce code -> withUser $ \_ -> withAgent (\a -> verifyNtfToken a nm token nonce code) >> ok_
APICheckToken token -> withUser $ \_ ->
CRNtfTokenStatus <$> withAgent (\a -> checkNtfToken a nm token)
APIDeleteToken token -> withUser $ \_ -> withAgent (`deleteNtfToken` token) >> ok_
APIGetNtfConns nonce encNtfInfo -> withUser $ \_ -> do
ntfInfos <- withAgent $ \a -> getNotificationConns a nonce encNtfInfo
(errs, ntfMsgs) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (getMsgConn db) (L.toList ntfInfos))
unless (null errs) $ toView $ CEvtChatErrors errs
pure $ CRNtfConns $ catMaybes ntfMsgs
where
getMsgConn :: DB.Connection -> NotificationInfo -> IO (Maybe NtfConn)
getMsgConn db NotificationInfo {ntfConnId, ntfDbQueueId, ntfMsgMeta = nMsgMeta} = do
let agentConnId = AgentConnId ntfConnId
mkNtfConn user connEntity = NtfConn {user, agentConnId, agentDbQueueId = ntfDbQueueId, connEntity, expectedMsg_ = expectedMsgInfo <$> nMsgMeta}
getUserByAConnId db agentConnId
$>>= \user -> fmap (mkNtfConn user) . eitherToMaybe <$> runExceptT (getConnectionEntity db vr user agentConnId)
APIGetConnNtfMessages connMsgs -> withUser $ \_ -> do
msgs <- lift $ withAgent' (`getConnectionMessages` connMsgs)
let ntfMsgs = L.map receivedMsgInfo msgs
pure $ CRConnNtfMessages ntfMsgs
GetUserProtoServers (AProtocolType p) -> withUser $ \user -> withServerProtocol p $ do
srvs <- withFastStore (`getUserServers` user)
liftIO $ CRUserServers user <$> groupByOperator (protocolServers p srvs)
SetUserProtoServers (AProtocolType (p :: SProtocolType p)) srvs -> withUser $ \user@User {userId} -> withServerProtocol p $ do
userServers_ <- liftIO . groupByOperator =<< withFastStore (`getUserServers` user)
case L.nonEmpty userServers_ of
Nothing -> throwCmdError "no servers"
Just userServers -> case srvs of
[] -> throwCmdError "no servers"
_ -> do
srvs' <- mapM aUserServer srvs
processChatCommand vr nm $ APISetUserServers userId $ L.map (updatedServers p srvs') userServers
where
aUserServer :: AProtoServerWithAuth -> CM (AUserServer p)
aUserServer (AProtoServerWithAuth p' srv) = case testEquality p p' of
Just Refl -> pure $ AUS SDBNew $ newUserServer srv
Nothing -> throwCmdError $ "incorrect server protocol: " <> B.unpack (strEncode srv)
APITestProtoServer userId srv@(AProtoServerWithAuth _ server) -> withUserId userId $ \user ->
lift $ CRServerTestResult user srv <$> withAgent' (\a -> testProtocolServer a nm (aUserId user) server)
TestProtoServer srv -> withUser $ \User {userId} ->
processChatCommand vr nm $ APITestProtoServer userId srv
APIGetServerOperators -> CRServerOperatorConditions <$> withFastStore getServerOperators
APISetServerOperators operators -> do
as <- asks randomAgentServers
(opsConds, srvs) <- withFastStore $ \db -> do
liftIO $ setServerOperators db operators
opsConds <- getServerOperators db
let ops = serverOperators opsConds
ops' = map Just ops <> [Nothing]
opDomains = operatorDomains ops
liftIO $ fmap (opsConds,) . mapM (getServers db as ops' opDomains) =<< getUsers db
lift $ withAgent' $ \a -> forM_ srvs $ \(auId, (smp', xftp')) -> do
setProtocolServers a auId smp'
setProtocolServers a auId xftp'
pure $ CRServerOperatorConditions opsConds
where
getServers :: DB.Connection -> RandomAgentServers -> [Maybe ServerOperator] -> [(Text, ServerOperator)] -> User -> IO (UserId, (NonEmpty (ServerCfg 'PSMP), NonEmpty (ServerCfg 'PXFTP)))
getServers db as ops opDomains user = do
smpSrvs <- getProtocolServers db SPSMP user
xftpSrvs <- getProtocolServers db SPXFTP user
uss <- groupByOperator (ops, smpSrvs, xftpSrvs)
pure $ (aUserId user,) $ useServers as opDomains uss
SetServerOperators operatorsRoles -> do
ops <- serverOperators <$> withFastStore getServerOperators
ops' <- mapM (updateOp ops) operatorsRoles
processChatCommand vr nm $ APISetServerOperators ops'
where
updateOp :: [ServerOperator] -> ServerOperatorRoles -> CM ServerOperator
updateOp ops r =
case find (\ServerOperator {operatorId = DBEntityId opId} -> operatorId' r == opId) ops of
Just op -> pure op {enabled = enabled' r, smpRoles = smpRoles' r, xftpRoles = xftpRoles' r}
Nothing -> throwError $ ChatErrorStore $ SEOperatorNotFound $ operatorId' r
APIGetUserServers userId -> withUserId userId $ \user -> withFastStore $ \db -> do
CRUserServers user <$> (liftIO . groupByOperator =<< getUserServers db user)
APISetUserServers userId userServers -> withUserId userId $ \user -> do
errors <- validateAllUsersServers userId $ L.toList userServers
unless (null errors) $ throwCmdError $ "user servers validation error(s): " <> show errors
uss <- withFastStore $ \db -> do
ts <- liftIO getCurrentTime
mapM (setUserServers db user ts) userServers
as <- asks randomAgentServers
lift $ withAgent' $ \a -> do
let auId = aUserId user
opDomains = operatorDomains $ mapMaybe operator' $ L.toList uss
(smp', xftp') = useServers as opDomains uss
setProtocolServers a auId smp'
setProtocolServers a auId xftp'
ok_
APIValidateServers userId userServers -> withUserId userId $ \user ->
CRUserServersValidation user <$> validateAllUsersServers userId userServers
APIGetUsageConditions -> do
(usageConditions, acceptedConditions) <- withFastStore $ \db -> do
usageConditions <- getCurrentUsageConditions db
acceptedConditions <- liftIO $ getLatestAcceptedConditions db
pure (usageConditions, acceptedConditions)
-- TODO if db commit is different from source commit, conditionsText should be nothing in response
pure
CRUsageConditions
{ usageConditions,
conditionsText = usageConditionsText,
acceptedConditions
}
APISetConditionsNotified condId -> do
currentTs <- liftIO getCurrentTime
withFastStore' $ \db -> setConditionsNotified db condId currentTs
ok_
APIAcceptConditions condId opIds -> withFastStore $ \db -> do
currentTs <- liftIO getCurrentTime
acceptConditions db condId opIds currentTs
CRServerOperatorConditions <$> getServerOperators db
APISetChatTTL userId (ChatRef cType chatId scope) newTTL_ ->
withUserId userId $ \user -> checkStoreNotChanged $ withChatLock "setChatTTL" $ do
(oldTTL_, globalTTL, ttlCount) <- withStore' $ \db ->
(,,) <$> getSetChatTTL db <*> getChatItemTTL db user <*> getChatTTLCount db user
let newTTL = fromMaybe globalTTL newTTL_
oldTTL = fromMaybe globalTTL oldTTL_
when (newTTL > 0 && (newTTL < oldTTL || oldTTL == 0)) $ do
lift $ setExpireCIFlag user False
expireChat user globalTTL `catchAllErrors` eToView
lift $ setChatItemsExpiration user globalTTL ttlCount
ok user
where
getSetChatTTL db = case cType of
CTDirect -> getDirectChatTTL db chatId <* setDirectChatTTL db chatId newTTL_
CTGroup | isNothing scope -> getGroupChatTTL db chatId <* setGroupChatTTL db chatId newTTL_
_ -> pure Nothing
expireChat user globalTTL = do
currentTs <- liftIO getCurrentTime
case cType of
CTDirect -> expireContactChatItems user vr globalTTL chatId
CTGroup | isNothing scope ->
let createdAtCutoff = addUTCTime (-43200 :: NominalDiffTime) currentTs
in expireGroupChatItems user vr globalTTL createdAtCutoff chatId
_ -> throwCmdError "not supported"
SetChatTTL chatName newTTL -> withUser' $ \user@User {userId} -> do
chatRef <- getChatRef user chatName
processChatCommand vr nm $ APISetChatTTL userId chatRef newTTL
GetChatTTL chatName -> withUser' $ \user -> do
-- TODO [knocking] support scope in CLI apis
ChatRef cType chatId _ <- getChatRef user chatName
ttl <- case cType of
CTDirect -> withFastStore' (`getDirectChatTTL` chatId)
CTGroup -> withFastStore' (`getGroupChatTTL` chatId)
_ -> throwCmdError "not supported"
pure $ CRChatItemTTL user ttl
APISetChatItemTTL userId newTTL -> withUserId userId $ \user ->
checkStoreNotChanged $
withChatLock "setChatItemTTL" $ do
(oldTTL, ttlCount) <- withFastStore' $ \db ->
(,) <$> getChatItemTTL db user <* setChatItemTTL db user newTTL <*> getChatTTLCount db user
when (newTTL > 0 && (newTTL < oldTTL || oldTTL == 0)) $ do
lift $ setExpireCIFlag user False
expireChatItems user newTTL True
lift $ setChatItemsExpiration user newTTL ttlCount
ok user
SetChatItemTTL newTTL_ -> withUser' $ \User {userId} -> do
processChatCommand vr nm $ APISetChatItemTTL userId newTTL_
APIGetChatItemTTL userId -> withUserId' userId $ \user -> do
ttl <- withFastStore' (`getChatItemTTL` user)
pure $ CRChatItemTTL user (Just ttl)
GetChatItemTTL -> withUser' $ \User {userId} -> do
processChatCommand vr nm $ APIGetChatItemTTL userId
APISetNetworkConfig cfg -> withUser' $ \_ -> lift (withAgent' (`setNetworkConfig` cfg)) >> ok_
APIGetNetworkConfig -> withUser' $ \_ ->
CRNetworkConfig <$> lift getNetworkConfig
SetNetworkConfig simpleNetCfg -> do
cfg <- (`updateNetworkConfig` simpleNetCfg) <$> lift getNetworkConfig
void . processChatCommand vr nm $ APISetNetworkConfig cfg
pure $ CRNetworkConfig cfg
APISetNetworkInfo info -> lift (withAgent' (`setUserNetworkInfo` info)) >> ok_
ReconnectAllServers -> withUser' $ \_ -> lift (withAgent' reconnectAllServers) >> ok_
ReconnectServer userId srv -> withUserId userId $ \user -> do
lift (withAgent' $ \a -> reconnectSMPServer a (aUserId user) srv)
ok_
APISetChatSettings (ChatRef cType chatId scope) chatSettings -> withUser $ \user -> case cType of
CTDirect -> do
ct <- withFastStore $ \db -> do
ct <- getContact db vr user chatId
liftIO $ updateContactSettings db user chatId chatSettings
pure ct
forM_ (contactConnId ct) $ \connId ->
withAgent $ \a -> toggleConnectionNtfs a connId (chatHasNtfs chatSettings)
ok user
CTGroup | isNothing scope -> do
ms <- withFastStore $ \db -> do
gInfo <- getGroupInfo db vr user chatId
ms <- liftIO $ getMembers db gInfo
liftIO $ updateGroupSettings db user chatId chatSettings
pure ms
forM_ (filter memberActive ms) $ \m -> forM_ (memberConnId m) $ \connId ->
withAgent (\a -> toggleConnectionNtfs a connId $ chatHasNtfs chatSettings) `catchAllErrors` eToView
ok user
where
getMembers db gInfo@GroupInfo {useRelays}
| isTrue useRelays = getGroupRelays db vr user gInfo
| otherwise = getGroupMembers db vr user gInfo
_ -> throwCmdError "not supported"
APISetMemberSettings gId gMemberId settings -> withUser $ \user -> do
m <- withFastStore $ \db -> do
liftIO $ updateGroupMemberSettings db user gId gMemberId settings
getGroupMember db vr user gId gMemberId
let ntfOn = not (memberBlocked m)
toggleNtf m ntfOn
ok user
APIContactInfo contactId -> withUser $ \user@User {userId} -> do
-- [incognito] print user's incognito profile for this contact
ct@Contact {activeConn} <- withFastStore $ \db -> getContact db vr user contactId
incognitoProfile <- case activeConn of
Nothing -> pure Nothing
Just Connection {customUserProfileId} ->
forM customUserProfileId $ \profileId -> withFastStore (\db -> getProfileById db userId profileId)
connectionStats <- mapM (withAgent . flip getConnectionServers) (contactConnId ct)
pure $ CRContactInfo user ct connectionStats (fmap fromLocalProfile incognitoProfile)
APIContactQueueInfo contactId -> withUser $ \user -> do
ct@Contact {activeConn} <- withFastStore $ \db -> getContact db vr user contactId
case activeConn of
Just conn -> getConnQueueInfo user conn
Nothing -> throwChatError $ CEContactNotActive ct
APIGroupInfo gId -> withUser $ \user ->
CRGroupInfo user <$> withFastStore (\db -> getGroupInfo db vr user gId)
APIGroupMemberInfo gId gMemberId -> withUser $ \user -> do
(g, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId
connectionStats <- mapM (withAgent . flip getConnectionServers) (memberConnId m)
pure $ CRGroupMemberInfo user g m connectionStats
APIGroupMemberQueueInfo gId gMemberId -> withUser $ \user -> do
GroupMember {activeConn} <- withFastStore $ \db -> getGroupMember db vr user gId gMemberId
case activeConn of
Just conn -> getConnQueueInfo user conn
Nothing -> throwChatError CEGroupMemberNotActive
APISwitchContact contactId -> withUser $ \user -> do
ct <- withFastStore $ \db -> getContact db vr user contactId
case contactConnId ct of
Just connId -> do
connectionStats <- withAgent $ \a -> switchConnectionAsync a "" connId
pure $ CRContactSwitchStarted user ct connectionStats
Nothing -> throwChatError $ CEContactNotActive ct
APISwitchGroupMember gId gMemberId -> withUser $ \user -> do
(g, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId
case memberConnId m of
Just connId -> do
connectionStats <- withAgent (\a -> switchConnectionAsync a "" connId)
pure $ CRGroupMemberSwitchStarted user g m connectionStats
_ -> throwChatError CEGroupMemberNotActive
APIAbortSwitchContact contactId -> withUser $ \user -> do
ct <- withFastStore $ \db -> getContact db vr user contactId
case contactConnId ct of
Just connId -> do
connectionStats <- withAgent $ \a -> abortConnectionSwitch a connId
pure $ CRContactSwitchAborted user ct connectionStats
Nothing -> throwChatError $ CEContactNotActive ct
APIAbortSwitchGroupMember gId gMemberId -> withUser $ \user -> do
(g, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId
case memberConnId m of
Just connId -> do
connectionStats <- withAgent $ \a -> abortConnectionSwitch a connId
pure $ CRGroupMemberSwitchAborted user g m connectionStats
_ -> throwChatError CEGroupMemberNotActive
APISyncContactRatchet contactId force -> withUser $ \user -> withContactLock "syncContactRatchet" contactId $ do
ct <- withFastStore $ \db -> getContact db vr user contactId
case contactConn ct of
Just conn@Connection {pqSupport} -> do
cStats@ConnectionStats {ratchetSyncState = rss} <- withAgent $ \a -> synchronizeRatchet a (aConnId conn) pqSupport force
createInternalChatItem user (CDDirectSnd ct) (CISndConnEvent $ SCERatchetSync rss Nothing) Nothing
pure $ CRContactRatchetSyncStarted user ct cStats
Nothing -> throwChatError $ CEContactNotActive ct
APISyncGroupMemberRatchet gId gMemberId force -> withUser $ \user -> withGroupLock "syncGroupMemberRatchet" gId $ do
(g, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId
case memberConnId m of
Just connId -> do
cStats@ConnectionStats {ratchetSyncState = rss} <- withAgent $ \a -> synchronizeRatchet a connId PQSupportOff force
(g', m', scopeInfo) <- mkGroupChatScope g m
createInternalChatItem user (CDGroupSnd g' scopeInfo) (CISndConnEvent . SCERatchetSync rss . Just $ groupMemberRef m') Nothing
pure $ CRGroupMemberRatchetSyncStarted user g' m' cStats
_ -> throwChatError CEGroupMemberNotActive
APIGetContactCode contactId -> withUser $ \user -> do
ct@Contact {activeConn} <- withFastStore $ \db -> getContact db vr user contactId
case activeConn of
Just conn@Connection {connId} -> do
code <- getConnectionCode $ aConnId conn
ct' <- case contactSecurityCode ct of
Just SecurityCode {securityCode}
| sameVerificationCode code securityCode -> pure ct
| otherwise -> do
withFastStore' $ \db -> setConnectionVerified db user connId Nothing
pure (ct :: Contact) {activeConn = Just $ (conn :: Connection) {connectionCode = Nothing}}
_ -> pure ct
pure $ CRContactCode user ct' code
Nothing -> throwChatError $ CEContactNotActive ct
APIGetGroupMemberCode gId gMemberId -> withUser $ \user -> do
(g, m@GroupMember {activeConn}) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId
case activeConn of
Just conn@Connection {connId} -> do
code <- getConnectionCode $ aConnId conn
m' <- case memberSecurityCode m of
Just SecurityCode {securityCode}
| sameVerificationCode code securityCode -> pure m
| otherwise -> do
withFastStore' $ \db -> setConnectionVerified db user connId Nothing
pure (m :: GroupMember) {activeConn = Just $ (conn :: Connection) {connectionCode = Nothing}}
_ -> pure m
pure $ CRGroupMemberCode user g m' code
_ -> throwChatError CEGroupMemberNotActive
APIVerifyContact contactId code -> withUser $ \user -> do
ct@Contact {activeConn} <- withFastStore $ \db -> getContact db vr user contactId
case activeConn of
Just conn -> verifyConnectionCode user conn code
Nothing -> throwChatError $ CEContactNotActive ct
APIVerifyGroupMember gId gMemberId code -> withUser $ \user -> do
GroupMember {activeConn} <- withFastStore $ \db -> getGroupMember db vr user gId gMemberId
case activeConn of
Just conn -> verifyConnectionCode user conn code
_ -> throwChatError CEGroupMemberNotActive
APIEnableContact contactId -> withUser $ \user -> do
ct@Contact {activeConn} <- withFastStore $ \db -> getContact db vr user contactId
case activeConn of
Just conn -> do
withFastStore' $ \db -> setAuthErrCounter db user conn 0
ok user
Nothing -> throwChatError $ CEContactNotActive ct
APIEnableGroupMember gId gMemberId -> withUser $ \user -> do
GroupMember {activeConn} <- withFastStore $ \db -> getGroupMember db vr user gId gMemberId
case activeConn of
Just conn -> do
withFastStore' $ \db -> setAuthErrCounter db user conn 0
ok user
_ -> throwChatError CEGroupMemberNotActive
SetShowMessages cName ntfOn -> updateChatSettings cName (\cs -> cs {enableNtfs = ntfOn})
SetSendReceipts cName rcptsOn_ -> updateChatSettings cName (\cs -> cs {sendRcpts = rcptsOn_})
SetShowMemberMessages gName mName showMessages -> withUser $ \user -> do
(gId, mId) <- getGroupAndMemberId user gName mName
gInfo <- withFastStore $ \db -> getGroupInfo db vr user gId
m <- withFastStore $ \db -> getGroupMember db vr user gId mId
let GroupInfo {membership = GroupMember {memberRole = membershipRole}} = gInfo
when (membershipRole >= GRModerator) $ throwChatError $ CECantBlockMemberForSelf gInfo m showMessages
let settings = (memberSettings m) {showMessages}
processChatCommand vr nm $ APISetMemberSettings gId mId settings
ContactInfo cName -> withContactName cName APIContactInfo
ShowGroupInfo gName -> withUser $ \user -> do
groupId <- withFastStore $ \db -> getGroupIdByName db user gName
processChatCommand vr nm $ APIGroupInfo groupId
GroupMemberInfo gName mName -> withMemberName gName mName APIGroupMemberInfo
ContactQueueInfo cName -> withContactName cName APIContactQueueInfo
GroupMemberQueueInfo gName mName -> withMemberName gName mName APIGroupMemberQueueInfo
SwitchContact cName -> withContactName cName APISwitchContact
SwitchGroupMember gName mName -> withMemberName gName mName APISwitchGroupMember
AbortSwitchContact cName -> withContactName cName APIAbortSwitchContact
AbortSwitchGroupMember gName mName -> withMemberName gName mName APIAbortSwitchGroupMember
SyncContactRatchet cName force -> withContactName cName $ \ctId -> APISyncContactRatchet ctId force
SyncGroupMemberRatchet gName mName force -> withMemberName gName mName $ \gId mId -> APISyncGroupMemberRatchet gId mId force
GetContactCode cName -> withContactName cName APIGetContactCode
GetGroupMemberCode gName mName -> withMemberName gName mName APIGetGroupMemberCode
VerifyContact cName code -> withContactName cName (`APIVerifyContact` code)
VerifyGroupMember gName mName code -> withMemberName gName mName $ \gId mId -> APIVerifyGroupMember gId mId code
EnableContact cName -> withContactName cName APIEnableContact
EnableGroupMember gName mName -> withMemberName gName mName $ \gId mId -> APIEnableGroupMember gId mId
ChatHelp section -> pure $ CRChatHelp section
Welcome -> withUser $ pure . CRWelcome
APIAddContact userId incognito -> withUserId userId $ \user -> do
-- [incognito] generate profile for connection
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
subMode <- chatReadVar subscriptionMode
let userData = contactShortLinkData (userProfileDirect user incognitoProfile Nothing True) Nothing
userLinkData = UserInvLinkData userData
-- TODO [certs rcv]
(connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True False SCMInvitation (Just userLinkData) Nothing IKPQOn subMode
ccLink' <- shortenCreatedLink ccLink
-- TODO PQ pass minVersion from the current range
conn <- withFastStore' $ \db -> createDirectConnection db user connId ccLink' Nothing ConnNew incognitoProfile subMode initialChatVersion PQSupportOn
pure $ CRInvitation user ccLink' conn
AddContact incognito -> withUser $ \User {userId} ->
processChatCommand vr nm $ APIAddContact userId incognito
APISetConnectionIncognito connId incognito -> withUser $ \user@User {userId} -> do
conn <- withFastStore $ \db -> getPendingContactConnection db userId connId
let PendingContactConnection {pccConnStatus, customUserProfileId} = conn
case (pccConnStatus, customUserProfileId, incognito) of
(ConnNew, Nothing, True) -> do
incognitoProfile <- liftIO generateRandomProfile
sLnk <- updatePCCShortLinkData conn $ userProfileDirect user (Just incognitoProfile) Nothing True
conn' <- withFastStore' $ \db -> do
pId <- createIncognitoProfile db user incognitoProfile
updatePCCIncognito db user conn (Just pId) sLnk
pure $ CRConnectionIncognitoUpdated user conn' (Just incognitoProfile)
(ConnNew, Just pId, False) -> do
sLnk <- updatePCCShortLinkData conn $ userProfileDirect user Nothing Nothing True
conn' <- withFastStore' $ \db -> do
deletePCCIncognitoProfile db user pId
updatePCCIncognito db user conn Nothing sLnk
pure $ CRConnectionIncognitoUpdated user conn' Nothing
_ -> throwChatError CEConnectionIncognitoChangeProhibited
APIChangeConnectionUser connId newUserId -> withUser $ \user@User {userId} -> do
conn <- withFastStore $ \db -> getPendingContactConnection db userId connId
let PendingContactConnection {pccConnStatus, connLinkInv} = conn
case (pccConnStatus, connLinkInv) of
(ConnNew, Just _ccLink) -> do
newUser <- privateGetUser newUserId
conn' <- recreateConn user conn newUser
pure $ CRConnectionUserChanged user conn conn' newUser
_ -> throwChatError CEConnectionUserChangeProhibited
where
recreateConn user conn@PendingContactConnection {customUserProfileId, connLinkInv} newUser = do
subMode <- chatReadVar subscriptionMode
let short = isJust $ connShortLink =<< connLinkInv
userLinkData_
| short = Just $ UserInvLinkData $ contactShortLinkData (userProfileDirect newUser Nothing Nothing True) Nothing
| otherwise = Nothing
-- TODO [certs rcv]
(agConnId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId newUser) True False SCMInvitation userLinkData_ Nothing IKPQOn subMode
ccLink' <- shortenCreatedLink ccLink
conn' <- withFastStore' $ \db -> do
deleteConnectionRecord db user connId
forM_ customUserProfileId $ \profileId ->
deletePCCIncognitoProfile db user profileId
createDirectConnection db newUser agConnId ccLink' Nothing ConnNew Nothing subMode initialChatVersion PQSupportOn
deleteAgentConnectionAsync (aConnId' conn)
pure conn'
APIConnectPlan userId (Just cLink) -> withUserId userId $ \user ->
uncurry (CRConnectionPlan user) <$> connectPlan user cLink
APIConnectPlan _ Nothing -> throwChatError CEInvalidConnReq
APIPrepareContact userId accLink contactSLinkData -> withUserId userId $ \user -> do
let ContactShortLinkData {profile, message, business} = contactSLinkData
welcomeSharedMsgId <- forM message $ \_ -> getSharedMsgId
case accLink of
ACCL SCMContact ccLink
| business -> do
let Profile {preferences} = profile
groupPreferences = maybe defaultBusinessGroupPrefs businessGroupPrefs preferences
groupProfile = businessGroupProfile profile groupPreferences
(gInfo, hostMember) <- withStore $ \db -> createPreparedGroup db vr user groupProfile True ccLink welcomeSharedMsgId
void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing (Just epochStart)
let cd = CDGroupRcv gInfo Nothing hostMember
createItem sharedMsgId content = createChatItem user cd True content sharedMsgId Nothing
cInfo = GroupChat gInfo Nothing
void $ createGroupFeatureItems_ user cd True CIRcvGroupFeature gInfo
aci <- mapM (createItem welcomeSharedMsgId . CIRcvMsgContent) message
let chat = case aci of
Just (AChatItem SCTGroup dir _ ci) -> Chat cInfo [CChatItem dir ci] emptyChatStats {unreadCount = 1, minUnreadItemId = chatItemId' ci}
_ -> Chat cInfo [] emptyChatStats
pure $ CRNewPreparedChat user $ AChat SCTGroup chat
ACCL _ (CCLink cReq _) -> do
ct <- withStore $ \db -> createPreparedContact db vr user profile accLink welcomeSharedMsgId
void $ createChatItem user (CDDirectSnd ct) False CIChatBanner Nothing (Just epochStart)
let cd = CDDirectRcv ct
createItem sharedMsgId content = createChatItem user cd False content sharedMsgId Nothing
cInfo = DirectChat ct
void $ createItem Nothing $ CIRcvDirectE2EEInfo $ E2EInfo $ connRequestPQEncryption cReq
void $ createFeatureEnabledItems_ user ct
aci <- mapM (createItem welcomeSharedMsgId . CIRcvMsgContent) message
let chat = case aci of
Just (AChatItem SCTDirect dir _ ci) -> Chat cInfo [CChatItem dir ci] emptyChatStats {unreadCount = 1, minUnreadItemId = chatItemId' ci}
_ -> Chat cInfo [] emptyChatStats
pure $ CRNewPreparedChat user $ AChat SCTDirect chat
APIPrepareGroup userId ccLink groupSLinkData -> withUserId userId $ \user -> do
let GroupShortLinkData {groupProfile = gp@GroupProfile {description}} = groupSLinkData
welcomeSharedMsgId <- forM description $ \_ -> getSharedMsgId
(gInfo, hostMember) <- withStore $ \db -> createPreparedGroup db vr user gp False ccLink welcomeSharedMsgId
void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing (Just epochStart)
let cd = CDGroupRcv gInfo Nothing hostMember
cInfo = GroupChat gInfo Nothing
void $ createGroupFeatureItems_ user cd True CIRcvGroupFeature gInfo
aci <- forM description $ \descr -> createChatItem user cd True (CIRcvMsgContent $ MCText descr) welcomeSharedMsgId Nothing
let chat = case aci of
Just (AChatItem SCTGroup dir _ ci) -> Chat cInfo [CChatItem dir ci] emptyChatStats {unreadCount = 1, minUnreadItemId = chatItemId' ci}
_ -> Chat cInfo [] emptyChatStats
pure $ CRNewPreparedChat user $ AChat SCTGroup chat
APIChangePreparedContactUser contactId newUserId -> withUser $ \user -> do
ct@Contact {preparedContact} <- withFastStore $ \db -> getContact db vr user contactId
when (isNothing preparedContact) $ throwCmdError "contact doesn't have link to connect"
when (isJust $ contactConn ct) $ throwCmdError "contact already has connection"
newUser <- privateGetUser newUserId
ct' <- withFastStore $ \db -> updatePreparedContactUser db vr user ct newUser
-- create changed feature items (new user may have different preferences)
lift $ createContactChangedFeatureItems user ct ct'
pure $ CRContactUserChanged user ct newUser ct'
APIChangePreparedGroupUser groupId newUserId -> withUser $ \user -> do
(gInfo, hostMember) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user groupId <*> getHostMember db vr user groupId
when (isNothing $ preparedGroup gInfo) $ throwCmdError "group doesn't have link to connect"
when (isJust $ memberConn hostMember) $ throwCmdError "host member already has connection"
newUser <- privateGetUser newUserId
gInfo' <- withFastStore $ \db -> updatePreparedGroupUser db vr user gInfo hostMember newUser
pure $ CRGroupUserChanged user gInfo newUser gInfo'
APIConnectPreparedContact contactId incognito msgContent_ -> withUser $ \user -> do
ct@Contact {preparedContact} <- withFastStore $ \db -> getContact db vr user contactId
case preparedContact of
Nothing -> throwCmdError "contact doesn't have link to connect"
Just PreparedContact {connLinkToConnect = ACCL SCMInvitation ccLink} -> do
(_, customUserProfile) <- connectViaInvitation user incognito ccLink (Just contactId) `catchAllErrors` \e -> do
-- get updated contact, in case connection was started - in UI it would lock ability to change
-- user or incognito profile for contact, in case server received request while client got network error
ct' <- withFastStore $ \db -> getContact db vr user contactId
toView $ CEvtChatInfoUpdated user (AChatInfo SCTDirect $ DirectChat ct')
throwError e
-- get updated contact with connection
ct' <- withFastStore $ \db -> getContact db vr user contactId
-- create changed feature items (connecting incognito sends default preferences, instead of user preferences)
lift . when incognito $ createContactChangedFeatureItems user ct ct'
forM_ msgContent_ $ \mc -> do
let evt = XMsgNew $ MCSimple (extMsgContent mc Nothing)
(msg, _) <- sendDirectContactMessage user ct' evt
ci <- saveSndChatItem user (CDDirectSnd ct') msg (CISndMsgContent mc)
toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct') ci]
pure $ CRStartedConnectionToContact user ct' customUserProfile
Just PreparedContact {connLinkToConnect = ACCL SCMContact ccLink, welcomeSharedMsgId, requestSharedMsgId} -> do
msg_ <- forM msgContent_ $ \mc -> case requestSharedMsgId of
Just smId -> pure (smId, mc)
Nothing -> do
smId <- getSharedMsgId
withFastStore' $ \db -> setRequestSharedMsgIdForContact db contactId smId
pure (smId, mc)
r <- connectViaContact user (Just $ PCEContact ct) incognito ccLink welcomeSharedMsgId msg_ `catchAllErrors` \e -> do
-- get updated contact, in case connection was started - in UI it would lock ability to change
-- user or incognito profile for contact, in case server received request while client got network error
ct' <- withFastStore $ \db -> getContact db vr user contactId
toView $ CEvtChatInfoUpdated user (AChatInfo SCTDirect $ DirectChat ct')
throwError e
case r of
CVRSentInvitation _conn customUserProfile -> do
-- get updated contact with connection
ct' <- withFastStore $ \db -> getContact db vr user contactId
-- create changed feature items (connecting incognito sends default preferences, instead of user preferences)
lift . when incognito $ createContactChangedFeatureItems user ct ct'
forM_ msg_ $ \(sharedMsgId, mc) -> do
ci <- createChatItem user (CDDirectSnd ct') False (CISndMsgContent mc) (Just sharedMsgId) Nothing
toView $ CEvtNewChatItems user [ci]
pure $ CRStartedConnectionToContact user ct' customUserProfile
CVRConnectedContact ct' -> pure $ CRContactAlreadyExists user ct'
APIConnectPreparedGroup groupId incognito msgContent_ -> withUser $ \user -> do
(gInfo, hostMember) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user groupId <*> getHostMember db vr user groupId
case preparedGroup gInfo of
Nothing -> throwCmdError "group doesn't have link to connect"
Just PreparedGroup {connLinkToConnect, welcomeSharedMsgId, requestSharedMsgId} -> do
msg_ <- forM msgContent_ $ \mc -> case requestSharedMsgId of
Just smId -> pure (smId, mc)
Nothing -> do
smId <- getSharedMsgId
withFastStore' $ \db -> setRequestSharedMsgIdForGroup db groupId smId
pure (smId, mc)
r <- connectViaContact user (Just $ PCEGroup gInfo hostMember) incognito connLinkToConnect welcomeSharedMsgId msg_ `catchAllErrors` \e -> do
-- get updated group info, in case connection was started (connLinkPreparedConnection) - in UI it would lock ability to change
-- user or incognito profile for group or business chat, in case server received request while client got network error
gInfo' <- withFastStore $ \db -> getGroupInfo db vr user groupId
toView $ CEvtChatInfoUpdated user (AChatInfo SCTGroup $ GroupChat gInfo' Nothing)
throwError e
case r of
CVRSentInvitation _conn customUserProfile -> do
-- get updated group info (connLinkStartedConnection and incognito membership)
gInfo' <- withFastStore $ \db -> do
liftIO $ setPreparedGroupStartedConnection db groupId
getGroupInfo db vr user groupId
forM_ msg_ $ \(sharedMsgId, mc) -> do
ci <- createChatItem user (CDGroupSnd gInfo' Nothing) False (CISndMsgContent mc) (Just sharedMsgId) Nothing
toView $ CEvtNewChatItems user [ci]
pure $ CRStartedConnectionToGroup user gInfo' customUserProfile
CVRConnectedContact _ct -> throwChatError $ CEException "contact already exists when connecting to group"
APIConnect userId incognito (Just acl) -> withUserId userId $ \user -> case acl of
ACCL SCMInvitation ccLink -> do
(conn, incognitoProfile) <- connectViaInvitation user incognito ccLink Nothing
let pcc = mkPendingContactConnection conn $ Just ccLink
pure $ CRSentConfirmation user pcc incognitoProfile
ACCL SCMContact ccLink ->
connectViaContact user Nothing incognito ccLink Nothing Nothing >>= \case
CVRConnectedContact ct -> pure $ CRContactAlreadyExists user ct
CVRSentInvitation conn incognitoProfile -> pure $ CRSentInvitation user (mkPendingContactConnection conn Nothing) incognitoProfile
APIConnect _ _ Nothing -> throwChatError CEInvalidConnReq
Connect incognito (Just cLink@(ACL m cLink')) -> withUser $ \user -> do
(ccLink, plan) <- connectPlan user cLink `catchAllErrors` \e -> case cLink' of CLFull cReq -> pure (ACCL m (CCLink cReq Nothing), CPInvitationLink (ILPOk Nothing)); _ -> throwError e
connectWithPlan user incognito ccLink plan
Connect _ Nothing -> throwChatError CEInvalidConnReq
APIConnectContactViaAddress userId incognito contactId -> withUserId userId $ \user -> do
ct@Contact {profile = LocalProfile {contactLink}} <- withFastStore $ \db -> getContact db vr user contactId
ccLink <- case contactLink of
Just (CLFull cReq) -> pure $ CCLink cReq Nothing
Just (CLShort sLnk) -> do
(cReq, _cData) <- getShortLinkConnReq user sLnk
pure $ CCLink cReq $ Just sLnk
Nothing -> throwCmdError "no address in contact profile"
connectContactViaAddress user incognito ct ccLink `catchAllErrors` \e -> do
-- get updated contact, in case connection was started - in UI it would lock ability to change incognito choice
-- on next connection attempt, in case server received request while client got network error
ct' <- withFastStore $ \db -> getContact db vr user contactId
toView $ CEvtChatInfoUpdated user (AChatInfo SCTDirect $ DirectChat ct')
throwError e
ConnectSimplex incognito -> withUser $ \user -> do
plan <- contactRequestPlan user adminContactReq Nothing `catchAllErrors` const (pure $ CPContactAddress (CAPOk Nothing))
connectWithPlan user incognito (ACCL SCMContact (CCLink adminContactReq Nothing)) plan
DeleteContact cName cdm -> withContactName cName $ \ctId -> APIDeleteChat (ChatRef CTDirect ctId Nothing) cdm
ClearContact cName -> withContactName cName $ \chatId -> APIClearChat $ ChatRef CTDirect chatId Nothing
APIListContacts userId -> withUserId userId $ \user ->
CRContactsList user <$> withFastStore' (\db -> getUserContacts db vr user)
ListContacts -> withUser $ \User {userId} ->
processChatCommand vr nm $ APIListContacts userId
APICreateMyAddress userId -> withUserId userId $ \user -> do
withFastStore' (\db -> runExceptT $ getUserAddress db user) >>= \case
Left SEUserContactLinkNotFound -> pure ()
Left e -> throwError $ ChatErrorStore e
Right _ -> throwError $ ChatErrorStore SEDuplicateContactLink
subMode <- chatReadVar subscriptionMode
let userData = contactShortLinkData (userProfileDirect user Nothing Nothing True) Nothing
userLinkData = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData}
-- TODO [certs rcv]
(connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True True SCMContact (Just userLinkData) Nothing IKPQOn subMode
ccLink' <- shortenCreatedLink ccLink
withFastStore $ \db -> createUserContactLink db user connId ccLink' subMode
pure $ CRUserContactLinkCreated user ccLink'
CreateMyAddress -> withUser $ \User {userId} ->
processChatCommand vr nm $ APICreateMyAddress userId
APIDeleteMyAddress userId -> withUserId userId $ \user@User {profile = p} -> do
conn <- withFastStore $ \db -> getUserAddressConnection db vr user
withChatLock "deleteMyAddress" $ do
deleteAgentConnectionAsync $ aConnId conn
withFastStore' (`deleteUserAddress` user)
let p' = (fromLocalProfile p :: Profile) {contactLink = Nothing}
r <- updateProfile_ user p' False $ withFastStore' $ \db -> setUserProfileContactLink db user Nothing
let user' = case r of
CRUserProfileUpdated u' _ _ _ -> u'
_ -> user
pure $ CRUserContactLinkDeleted user'
DeleteMyAddress -> withUser $ \User {userId} ->
processChatCommand vr nm $ APIDeleteMyAddress userId
APIShowMyAddress userId -> withUserId' userId $ \user ->
CRUserContactLink user <$> withFastStore (`getUserAddress` user)
ShowMyAddress -> withUser' $ \User {userId} ->
processChatCommand vr nm $ APIShowMyAddress userId
APIAddMyAddressShortLink userId -> withUserId' userId $ \user ->
CRUserContactLink user <$> (withFastStore (`getUserAddress` user) >>= setMyAddressData user)
APISetProfileAddress userId False -> withUserId userId $ \user@User {profile = p} -> do
let p' = (fromLocalProfile p :: Profile) {contactLink = Nothing}
updateProfile_ user p' True $ withFastStore' $ \db -> setUserProfileContactLink db user Nothing
APISetProfileAddress userId True -> withUserId userId $ \user@User {profile = p} -> do
ucl <- withFastStore (`getUserAddress` user)
-- TODO [short links] replace with short links
let p' = (fromLocalProfile p :: Profile) {contactLink = Just $ profileContactLink ucl}
updateProfile_ user p' True $ withFastStore' $ \db -> setUserProfileContactLink db user $ Just ucl
SetProfileAddress onOff -> withUser $ \User {userId} ->
processChatCommand vr nm $ APISetProfileAddress userId onOff
APISetAddressSettings userId settings@AddressSettings {businessAddress, autoAccept} -> withUserId userId $ \user -> do
ucl@UserContactLink {userContactLinkId, shortLinkDataSet, addressSettings} <- withFastStore (`getUserAddress` user)
forM_ autoAccept $ \AutoAccept {acceptIncognito} -> do
when (shortLinkDataSet && acceptIncognito) $ throwCmdError "incognito not allowed for address with short link data"
when (businessAddress && acceptIncognito) $ throwCmdError "requests to business address cannot be accepted incognito"
if addressSettings == settings
then pure $ CRUserContactLinkUpdated user ucl
else do
let ucl' = ucl {addressSettings = settings}
ucl'' <- if shortLinkDataSet then setMyAddressData user ucl' else pure ucl'
withFastStore' $ \db -> updateUserAddressSettings db userContactLinkId settings
pure $ CRUserContactLinkUpdated user ucl''
SetAddressSettings settings -> withUser $ \User {userId} ->
processChatCommand vr nm $ APISetAddressSettings userId settings
AcceptContact incognito cName -> withUser $ \User {userId} -> do
connReqId <- withFastStore $ \db -> getContactRequestIdByName db userId cName
processChatCommand vr nm $ APIAcceptContact incognito connReqId
RejectContact cName -> withUser $ \User {userId} -> do
connReqId <- withFastStore $ \db -> getContactRequestIdByName db userId cName
processChatCommand vr nm $ APIRejectContact connReqId
ForwardMessage toChatName fromContactName forwardedMsg -> withUser $ \user -> do
contactId <- withFastStore $ \db -> getContactIdByName db user fromContactName
forwardedItemId <- withFastStore $ \db -> getDirectChatItemIdByText' db user contactId forwardedMsg
toChatRef <- getChatRef user toChatName
processChatCommand vr nm $ APIForwardChatItems toChatRef (ChatRef CTDirect contactId Nothing) (forwardedItemId :| []) Nothing
ForwardGroupMessage toChatName fromGroupName fromMemberName_ forwardedMsg -> withUser $ \user -> do
groupId <- withFastStore $ \db -> getGroupIdByName db user fromGroupName
forwardedItemId <- withFastStore $ \db -> getGroupChatItemIdByText db user groupId fromMemberName_ forwardedMsg
toChatRef <- getChatRef user toChatName
processChatCommand vr nm $ APIForwardChatItems toChatRef (ChatRef CTGroup groupId Nothing) (forwardedItemId :| []) Nothing
ForwardLocalMessage toChatName forwardedMsg -> withUser $ \user -> do
folderId <- withFastStore (`getUserNoteFolderId` user)
forwardedItemId <- withFastStore $ \db -> getLocalChatItemIdByText' db user folderId forwardedMsg
toChatRef <- getChatRef user toChatName
processChatCommand vr nm $ APIForwardChatItems toChatRef (ChatRef CTLocal folderId Nothing) (forwardedItemId :| []) Nothing
SendMessage sendName msg -> withUser $ \user -> do
let mc = MCText msg
case sendName of
SNDirect name ->
withFastStore' (\db -> runExceptT $ getContactIdByName db user name) >>= \case
Right ctId -> do
let sendRef = SRDirect ctId
processChatCommand vr nm $ APISendMessages sendRef False Nothing [composedMessage Nothing mc]
Left _ ->
withFastStore' (\db -> runExceptT $ getActiveMembersByName db vr user name) >>= \case
Right [(gInfo, member)] -> do
let GroupInfo {localDisplayName = gName} = gInfo
GroupMember {localDisplayName = mName} = member
processChatCommand vr nm $ SendMemberContactMessage gName mName msg
Right (suspectedMember : _) ->
throwChatError $ CEContactNotFound name (Just suspectedMember)
_ ->
throwChatError $ CEContactNotFound name Nothing
SNGroup name scope_ -> do
(gId, cScope_, mentions) <- withFastStore $ \db -> do
gId <- getGroupIdByName db user name
cScope_ <-
forM scope_ $ \(GSNMemberSupport mName_) ->
GCSMemberSupport <$> mapM (getGroupMemberIdByName db user gId) mName_
(gId,cScope_,) <$> liftIO (getMessageMentions db user gId msg)
let sendRef = SRGroup gId cScope_
processChatCommand vr nm $ APISendMessages sendRef False Nothing [ComposedMessage Nothing Nothing mc mentions]
SNLocal -> do
folderId <- withFastStore (`getUserNoteFolderId` user)
processChatCommand vr nm $ APICreateChatItems folderId [composedMessage Nothing mc]
SendMemberContactMessage gName mName msg -> withUser $ \user -> do
(gId, mId) <- getGroupAndMemberId user gName mName
m <- withFastStore $ \db -> getGroupMember db vr user gId mId
let mc = MCText msg
case memberContactId m of
Nothing -> do
g <- withFastStore $ \db -> getGroupInfo db vr user gId
unless (groupFeatureUserAllowed SGFDirectMessages g) $ throwCmdError "direct messages not allowed"
toView $ CEvtNoMemberContactCreating user g m
processChatCommand vr nm (APICreateMemberContact gId mId) >>= \case
CRNewMemberContact _ ct@Contact {contactId} _ _ -> do
toViewTE $ TENewMemberContact user ct g m
processChatCommand vr nm $ APISendMemberContactInvitation contactId (Just mc)
cr -> pure cr
Just ctId -> do
let sendRef = SRDirect ctId
processChatCommand vr nm $ APISendMessages sendRef False Nothing [composedMessage Nothing mc]
AcceptMemberContact cName -> withUser $ \user -> do
contactId <- withFastStore $ \db -> getContactIdByName db user cName
processChatCommand vr nm $ APIAcceptMemberContact contactId
SendLiveMessage chatName msg -> withUser $ \user -> do
(chatRef, mentions) <- getChatRefAndMentions user chatName msg
withSendRef chatRef $ \sendRef -> do
let mc = MCText msg
processChatCommand vr nm $ APISendMessages sendRef True Nothing [ComposedMessage Nothing Nothing mc mentions]
SendMessageBroadcast mc -> withUser $ \user -> do
contacts <- withFastStore' $ \db -> getUserContacts db vr user
withChatLock "sendMessageBroadcast" $ do
let ctConns_ = L.nonEmpty $ foldr addContactConn [] contacts
case ctConns_ of
Nothing -> do
timestamp <- liftIO getCurrentTime
pure CRBroadcastSent {user, msgContent = mc, successes = 0, failures = 0, timestamp}
Just (ctConns :: NonEmpty (Contact, Connection)) -> do
let idsEvts = L.map ctSndEvent ctConns
-- TODO Broadcast rework
-- In createNewSndMessage and encodeChatMessage we could use Nothing for sharedMsgId,
-- then we could reuse message body across broadcast.
-- Encoding different sharedMsgId and reusing body is meaningless as referencing will not work anyway.
-- As an improvement, single message record with its sharedMsgId could be created for new "broadcast" entity.
-- Then all recipients could refer to broadcast message using same sharedMsgId.
sndMsgs <- lift $ createSndMessages idsEvts
let msgReqs_ :: NonEmpty (Either ChatError ChatMsgReq) = L.zipWith (fmap . ctMsgReq) ctConns sndMsgs
(errs, ctSndMsgs :: [(Contact, SndMessage)]) <-
partitionEithers . L.toList . zipWith3' combineResults ctConns sndMsgs <$> deliverMessagesB msgReqs_
timestamp <- liftIO getCurrentTime
lift . void $ withStoreBatch' $ \db -> map (createCI db user timestamp) ctSndMsgs
pure CRBroadcastSent {user, msgContent = mc, successes = length ctSndMsgs, failures = length errs, timestamp}
where
addContactConn :: Contact -> [(Contact, Connection)] -> [(Contact, Connection)]
addContactConn ct ctConns = case contactSendConn_ ct of
Right conn | directOrUsed ct -> (ct, conn) : ctConns
_ -> ctConns
ctSndEvent :: (Contact, Connection) -> (ConnOrGroupId, ChatMsgEvent 'Json)
ctSndEvent (_, Connection {connId}) = (ConnectionId connId, XMsgNew $ MCSimple (extMsgContent mc Nothing))
ctMsgReq :: (Contact, Connection) -> SndMessage -> ChatMsgReq
ctMsgReq (_, conn) SndMessage {msgId, msgBody} = (conn, MsgFlags {notification = hasNotification XMsgNew_}, (vrValue msgBody, [msgId]))
combineResults :: (Contact, Connection) -> Either ChatError SndMessage -> Either ChatError ([Int64], PQEncryption) -> Either ChatError (Contact, SndMessage)
combineResults (ct, _) (Right msg') (Right _) = Right (ct, msg')
combineResults _ (Left e) _ = Left e
combineResults _ _ (Left e) = Left e
createCI :: DB.Connection -> User -> UTCTime -> (Contact, SndMessage) -> IO ()
createCI db user createdAt (ct, sndMsg) =
void $ createNewSndChatItem db user (CDDirectSnd ct) sndMsg (CISndMsgContent mc) Nothing Nothing Nothing False createdAt
SendMessageQuote cName (AMsgDirection msgDir) quotedMsg msg -> withUser $ \user@User {userId} -> do
contactId <- withFastStore $ \db -> getContactIdByName db user cName
quotedItemId <- withFastStore $ \db -> getDirectChatItemIdByText db userId contactId msgDir quotedMsg
let mc = MCText msg
processChatCommand vr nm $ APISendMessages (SRDirect contactId) False Nothing [ComposedMessage Nothing (Just quotedItemId) mc M.empty]
DeleteMessage chatName deletedMsg -> withUser $ \user -> do
chatRef <- getChatRef user chatName
deletedItemId <- getSentChatItemIdByText user chatRef deletedMsg
processChatCommand vr nm $ APIDeleteChatItem chatRef (deletedItemId :| []) CIDMBroadcast
DeleteMemberMessage gName mName deletedMsg -> withUser $ \user -> do
gId <- withFastStore $ \db -> getGroupIdByName db user gName
deletedItemId <- withFastStore $ \db -> getGroupChatItemIdByText db user gId (Just mName) deletedMsg
processChatCommand vr nm $ APIDeleteMemberChatItem gId (deletedItemId :| [])
EditMessage chatName editedMsg msg -> withUser $ \user -> do
(chatRef, mentions) <- getChatRefAndMentions user chatName msg
editedItemId <- getSentChatItemIdByText user chatRef editedMsg
let mc = MCText msg
processChatCommand vr nm $ APIUpdateChatItem chatRef editedItemId False $ UpdatedMessage mc mentions
UpdateLiveMessage chatName chatItemId live msg -> withUser $ \user -> do
(chatRef, mentions) <- getChatRefAndMentions user chatName msg
let mc = MCText msg
processChatCommand vr nm $ APIUpdateChatItem chatRef chatItemId live $ UpdatedMessage mc mentions
ReactToMessage add reaction chatName msg -> withUser $ \user -> do
chatRef <- getChatRef user chatName
chatItemId <- getChatItemIdByText user chatRef msg
processChatCommand vr nm $ APIChatItemReaction chatRef chatItemId add reaction
APINewGroup userId incognito gProfile@GroupProfile {displayName} -> withUserId userId $ \user -> do
checkValidName displayName
gVar <- asks random
-- [incognito] generate incognito profile for group membership
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
gInfo <- withFastStore $ \db -> createNewGroup db vr gVar user gProfile incognitoProfile
let cd = CDGroupSnd gInfo Nothing
createInternalChatItem user cd CIChatBanner (Just epochStart)
createInternalChatItem user cd (CISndGroupE2EEInfo E2EInfo {pqEnabled = Just PQEncOff}) Nothing
createGroupFeatureItems user cd CISndGroupFeature gInfo
pure $ CRGroupCreated user gInfo
NewGroup incognito gProfile -> withUser $ \User {userId} ->
processChatCommand vr nm $ APINewGroup userId incognito gProfile
APIAddMember groupId contactId memRole -> withUser $ \user -> withGroupLock "addMember" groupId $ do
-- TODO for large groups: no need to load all members to determine if contact is a member
(group, contact) <- withFastStore $ \db -> (,) <$> getGroup db vr user groupId <*> getContact db vr user contactId
assertDirectAllowed user MDSnd contact XGrpInv_
let Group gInfo members = group
Contact {localDisplayName = cName} = contact
assertUserGroupRole gInfo $ max GRAdmin memRole
-- [incognito] forbid to invite contact to whom user is connected incognito
when (contactConnIncognito contact) $ throwChatError CEContactIncognitoCantInvite
-- [incognito] forbid to invite contacts if user joined the group using an incognito profile
when (incognitoMembership gInfo) $ throwChatError CEGroupIncognitoCantInvite
let sendInvitation = sendGrpInvitation user contact gInfo
case contactMember contact members of
Nothing -> do
gVar <- asks random
subMode <- chatReadVar subscriptionMode
-- TODO [certs rcv]
(agentConnId, (CCLink cReq _, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True False SCMInvitation Nothing Nothing IKPQOff subMode
member <- withFastStore $ \db -> createNewContactMember db gVar user gInfo contact memRole agentConnId cReq subMode
sendInvitation member cReq
pure $ CRSentGroupInvitation user gInfo contact member
Just member@GroupMember {groupMemberId, memberStatus, memberRole = mRole}
| memberStatus == GSMemInvited -> do
unless (mRole == memRole) $ withFastStore' $ \db -> updateGroupMemberRole db user member memRole
withFastStore' (\db -> getMemberInvitation db user groupMemberId) >>= \case
Just cReq -> do
sendInvitation member {memberRole = memRole} cReq
pure $ CRSentGroupInvitation user gInfo contact member {memberRole = memRole}
Nothing -> throwChatError $ CEGroupCantResendInvitation gInfo cName
| otherwise -> throwChatError $ CEGroupDuplicateMember cName
APIJoinGroup groupId enableNtfs -> withUser $ \user@User {userId} -> do
withGroupLock "joinGroup" groupId $ do
(invitation, ct) <- withFastStore $ \db -> do
inv@ReceivedGroupInvitation {fromMember} <- getGroupInvitation db vr user groupId
(inv,) <$> getContactViaMember db vr user fromMember
let ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g@GroupInfo {membership, chatSettings}} = invitation
GroupMember {memberId = membershipMemId} = membership
Contact {activeConn} = ct
case activeConn of
Just Connection {peerChatVRange} -> do
subMode <- chatReadVar subscriptionMode
dm <- encodeConnInfo $ XGrpAcpt membershipMemId
agentConnId <- case memberConn fromMember of
Nothing -> do
agentConnId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True connRequest PQSupportOff
let chatV = vr `peerConnChatVersion` peerChatVRange
void $ withFastStore' $ \db -> createMemberConnection db userId fromMember agentConnId chatV peerChatVRange subMode
pure agentConnId
Just conn -> pure $ aConnId conn
withFastStore' $ \db -> do
updateGroupMemberStatus db userId fromMember GSMemAccepted
updateGroupMemberStatus db userId membership GSMemAccepted
-- MFAll is default for new groups
unless (enableNtfs == MFAll) $ updateGroupSettings db user groupId chatSettings {enableNtfs}
void (withAgent $ \a -> joinConnection a nm (aUserId user) agentConnId (enableNtfs /= MFNone) connRequest dm PQSupportOff subMode)
`catchAllErrors` \e -> do
withFastStore' $ \db -> do
updateGroupMemberStatus db userId fromMember GSMemInvited
updateGroupMemberStatus db userId membership GSMemInvited
throwError e
updateCIGroupInvitationStatus user g CIGISAccepted `catchAllErrors` eToView
pure $ CRUserAcceptedGroupSent user g {membership = membership {memberStatus = GSMemAccepted}} Nothing
Nothing -> throwChatError $ CEContactNotActive ct
APIAcceptMember groupId gmId role -> withUser $ \user@User {userId} -> do
(gInfo, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user groupId <*> getGroupMemberById db vr user gmId
-- TODO check that user's role is > role, possibly restrict role to only observer and member
assertUserGroupRole gInfo GRModerator
case memberStatus m of
GSMemPendingApproval | memberCategory m == GCInviteeMember -> do -- only host can approve
let GroupInfo {groupProfile = GroupProfile {memberAdmission}} = gInfo
case memberConn m of
Just mConn ->
case memberAdmission >>= review of
Just MCAll -> do
introduceToModerators vr user gInfo m
withFastStore' $ \db -> updateGroupMemberStatus db userId m GSMemPendingReview
let m' = m {memberStatus = GSMemPendingReview}
pure $ CRMemberAccepted user gInfo m'
Nothing -> do
let msg = XGrpLinkAcpt GAAccepted role (memberId' m)
void $ sendDirectMemberMessage mConn msg groupId
introduceToRemaining vr user gInfo m {memberRole = role}
when (groupFeatureAllowed SGFHistory gInfo) $ sendHistory user gInfo m
(m', gInfo') <- withFastStore' $ \db -> do
m' <- updateGroupMemberAccepted db user m GSMemConnected role
gInfo' <- updateGroupMembersRequireAttention db user gInfo m m'
pure (m', gInfo')
-- create item in both scopes
createInternalChatItem user (CDGroupRcv gInfo' Nothing m') (CIRcvGroupEvent RGEMemberConnected) Nothing
let scopeInfo = Just GCSIMemberSupport {groupMember_ = Just m'}
gEvent = SGEMemberAccepted gmId (fromLocalProfile $ memberProfile m')
createInternalChatItem user (CDGroupSnd gInfo' scopeInfo) (CISndGroupEvent gEvent) Nothing
pure $ CRMemberAccepted user gInfo' m'
Nothing -> throwChatError CEGroupMemberNotActive
GSMemPendingReview -> do
let scope = Just $ GCSMemberSupport $ Just (groupMemberId' m)
modMs <- withFastStore' $ \db -> getGroupModerators db vr user gInfo
let rcpModMs' = filter memberCurrent modMs
msg = XGrpLinkAcpt GAAccepted role (memberId' m)
void $ sendGroupMessage user gInfo scope ([m] <> rcpModMs') msg
when (maxVersion (memberChatVRange m) < groupKnockingVersion) $
forM_ (memberConn m) $ \mConn -> do
let msg2 = XMsgNew $ MCSimple $ extMsgContent (MCText acceptedToGroupMessage) Nothing
void $ sendDirectMemberMessage mConn msg2 groupId
when (memberCategory m == GCInviteeMember) $ do
introduceToRemaining vr user gInfo m {memberRole = role}
when (groupFeatureAllowed SGFHistory gInfo) $ sendHistory user gInfo m
(m', gInfo') <- withFastStore' $ \db -> do
m' <- updateGroupMemberAccepted db user m newMemberStatus role
gInfo' <- updateGroupMembersRequireAttention db user gInfo m m'
pure (m', gInfo')
-- create item in both scopes
createInternalChatItem user (CDGroupRcv gInfo' Nothing m') (CIRcvGroupEvent RGEMemberConnected) Nothing
let scopeInfo = Just GCSIMemberSupport {groupMember_ = Just m'}
gEvent = SGEMemberAccepted gmId (fromLocalProfile $ memberProfile m')
createInternalChatItem user (CDGroupSnd gInfo' scopeInfo) (CISndGroupEvent gEvent) Nothing
pure $ CRMemberAccepted user gInfo' m'
where
newMemberStatus = case memberConn m of
Just c | connReady c -> GSMemConnected
_ -> GSMemAnnounced
_ -> throwCmdError "member should be pending approval and invitee, or pending review and not invitee"
APIDeleteMemberSupportChat groupId gmId -> withUser $ \user -> do
(gInfo, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user groupId <*> getGroupMemberById db vr user gmId
when (isNothing $ supportChat m) $ throwCmdError "member has no support chat"
when (memberPending m) $ throwCmdError "member is pending"
(gInfo', m') <- withFastStore' $ \db -> do
gInfo' <-
if gmRequiresAttention m
then decreaseGroupMembersRequireAttention db user gInfo
else pure gInfo
m' <- deleteGroupMemberSupportChat db m
pure (gInfo', m')
pure $ CRMemberSupportChatDeleted user gInfo' m'
APIMembersRole groupId memberIds newRole -> withUser $ \user ->
withGroupLock "memberRole" groupId $ do
-- TODO [channels fwd] possible optimization is to read only required members + relays
g@(Group gInfo members) <- withFastStore $ \db -> getGroup db vr user groupId
when (selfSelected gInfo) $ throwCmdError "can't change role for self"
let (invitedMems, currentMems, unchangedMems, maxRole, anyAdmin, anyPending) = selectMembers members
when (length invitedMems + length currentMems + length unchangedMems /= length memberIds) $ throwChatError CEGroupMemberNotFound
when (length memberIds > 1 && (anyAdmin || newRole >= GRAdmin)) $
throwCmdError "can't change role of multiple members when admins selected, or new role is admin"
when anyPending $ throwCmdError "can't change role of members pending approval"
assertUserGroupRole gInfo $ maximum ([GRAdmin, maxRole, newRole] :: [GroupMemberRole])
(errs1, changed1) <- changeRoleInvitedMems user gInfo invitedMems
(errs2, changed2, acis) <- changeRoleCurrentMems user g currentMems
unless (null acis) $ toView $ CEvtNewChatItems user acis
let errs = errs1 <> errs2
unless (null errs) $ toView $ CEvtChatErrors errs
pure $ CRMembersRoleUser {user, groupInfo = gInfo, members = changed1 <> changed2, toRole = newRole} -- same order is not guaranteed
where
selfSelected GroupInfo {membership} = elem (groupMemberId' membership) memberIds
selectMembers :: [GroupMember] -> ([GroupMember], [GroupMember], [GroupMember], GroupMemberRole, Bool, Bool)
selectMembers = foldr' addMember ([], [], [], GRObserver, False, False)
where
addMember m@GroupMember {groupMemberId, memberStatus, memberRole} (invited, current, unchanged, maxRole, anyAdmin, anyPending)
| groupMemberId `elem` memberIds =
let maxRole' = max maxRole memberRole
anyAdmin' = anyAdmin || memberRole >= GRAdmin
anyPending' = anyPending || memberPending m
in
if
| memberRole == newRole -> (invited, current, m : unchanged, maxRole', anyAdmin', anyPending')
| memberStatus == GSMemInvited -> (m : invited, current, unchanged, maxRole', anyAdmin', anyPending')
| otherwise -> (invited, m : current, unchanged, maxRole', anyAdmin', anyPending')
| otherwise = (invited, current, unchanged, maxRole, anyAdmin, anyPending)
changeRoleInvitedMems :: User -> GroupInfo -> [GroupMember] -> CM ([ChatError], [GroupMember])
changeRoleInvitedMems user gInfo memsToChange = do
-- not batched, as we need to send different invitations to different connections anyway
mems_ <- forM memsToChange $ \m -> (Right <$> changeRole m) `catchAllErrors` (pure . Left)
pure $ partitionEithers mems_
where
changeRole :: GroupMember -> CM GroupMember
changeRole m@GroupMember {groupMemberId, memberContactId, localDisplayName = cName} = do
withFastStore (\db -> (,) <$> mapM (getContact db vr user) memberContactId <*> liftIO (getMemberInvitation db user groupMemberId)) >>= \case
(Just ct, Just cReq) -> do
sendGrpInvitation user ct gInfo (m :: GroupMember) {memberRole = newRole} cReq
withFastStore' $ \db -> updateGroupMemberRole db user m newRole
pure (m :: GroupMember) {memberRole = newRole}
_ -> throwChatError $ CEGroupCantResendInvitation gInfo cName
changeRoleCurrentMems :: User -> Group -> [GroupMember] -> CM ([ChatError], [GroupMember], [AChatItem])
changeRoleCurrentMems user (Group gInfo members) memsToChange = case L.nonEmpty memsToChange of
Nothing -> pure ([], [], [])
Just memsToChange' -> do
let events = L.map (\GroupMember {memberId} -> XGrpMemRole memberId newRole) memsToChange'
recipients = filter memberCurrent members
(msgs_, _gsr) <- sendGroupMessages user gInfo Nothing recipients events
let itemsData = zipWith (fmap . sndItemData) memsToChange (L.toList msgs_)
cis_ <- saveSndChatItems user (CDGroupSnd gInfo Nothing) itemsData Nothing False
when (length cis_ /= length memsToChange) $ logError "changeRoleCurrentMems: memsToChange and cis_ length mismatch"
(errs, changed) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (updMember db) memsToChange)
let acis = map (AChatItem SCTGroup SMDSnd (GroupChat gInfo Nothing)) $ rights cis_
pure (errs, changed, acis)
where
sndItemData :: GroupMember -> SndMessage -> NewSndChatItemData c
sndItemData GroupMember {groupMemberId, memberProfile} msg =
let content = CISndGroupEvent $ SGEMemberRole groupMemberId (fromLocalProfile memberProfile) newRole
ts = ciContentTexts content
in NewSndChatItemData msg content ts M.empty Nothing Nothing Nothing
updMember db m = do
updateGroupMemberRole db user m newRole
pure (m :: GroupMember) {memberRole = newRole}
APIBlockMembersForAll groupId memberIds blockFlag -> withUser $ \user ->
withGroupLock "blockForAll" groupId $ do
-- TODO [channels fwd] possible optimization is to read only required members + relays
Group gInfo members <- withFastStore $ \db -> getGroup db vr user groupId
when (selfSelected gInfo) $ throwCmdError "can't block/unblock self"
-- TODO [channels fwd] consider sending restriction to all members (remove filtering), as we do in delivery jobs
let (blockMems, remainingMems, maxRole, anyAdmin, anyPending) = selectMembers members
when (length blockMems /= length memberIds) $ throwChatError CEGroupMemberNotFound
when (length memberIds > 1 && anyAdmin) $ throwCmdError "can't block/unblock multiple members when admins selected"
when anyPending $ throwCmdError "can't block/unblock members pending approval"
assertUserGroupRole gInfo $ max GRModerator maxRole
blockMembers user gInfo blockMems remainingMems
where
selfSelected GroupInfo {membership} = elem (groupMemberId' membership) memberIds
selectMembers :: [GroupMember] -> ([GroupMember], [GroupMember], GroupMemberRole, Bool, Bool)
selectMembers = foldr' addMember ([], [], GRObserver, False, False)
where
addMember m@GroupMember {groupMemberId, memberRole} (block, remaining, maxRole, anyAdmin, anyPending)
| groupMemberId `elem` memberIds =
let maxRole' = max maxRole memberRole
anyAdmin' = anyAdmin || memberRole >= GRAdmin
anyPending' = anyPending || memberPending m
in (m : block, remaining, maxRole', anyAdmin', anyPending')
| otherwise = (block, m : remaining, maxRole, anyAdmin, anyPending)
blockMembers :: User -> GroupInfo -> [GroupMember] -> [GroupMember] -> CM ChatResponse
blockMembers user gInfo blockMems remainingMems = case L.nonEmpty blockMems of
Nothing -> throwCmdError "no members to block/unblock"
Just blockMems' -> do
let mrs = if blockFlag then MRSBlocked else MRSUnrestricted
events = L.map (\GroupMember {memberId} -> XGrpMemRestrict memberId MemberRestrictions {restriction = mrs}) blockMems'
recipients = filter memberCurrent remainingMems
(msgs_, _gsr) <- sendGroupMessages_ user gInfo recipients events
let itemsData = zipWith (fmap . sndItemData) blockMems (L.toList msgs_)
cis_ <- saveSndChatItems user (CDGroupSnd gInfo Nothing) itemsData Nothing False
when (length cis_ /= length blockMems) $ logError "blockMembers: blockMems and cis_ length mismatch"
let acis = map (AChatItem SCTGroup SMDSnd (GroupChat gInfo Nothing)) $ rights cis_
unless (null acis) $ toView $ CEvtNewChatItems user acis
(errs, blocked) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (updateGroupMemberBlocked db user gInfo mrs) blockMems)
unless (null errs) $ toView $ CEvtChatErrors errs
-- TODO not batched - requires agent batch api
forM_ blocked $ \m -> toggleNtf m (not blockFlag)
pure CRMembersBlockedForAllUser {user, groupInfo = gInfo, members = blocked, blocked = blockFlag}
where
sndItemData :: GroupMember -> SndMessage -> NewSndChatItemData c
sndItemData GroupMember {groupMemberId, memberProfile} msg =
let content = CISndGroupEvent $ SGEMemberBlocked groupMemberId (fromLocalProfile memberProfile) blockFlag
ts = ciContentTexts content
in NewSndChatItemData msg content ts M.empty Nothing Nothing Nothing
APIRemoveMembers {groupId, groupMemberIds, withMessages} -> withUser $ \user ->
withGroupLock "removeMembers" groupId $ do
-- TODO [channels fwd] possible optimization is to read only required members + relays
Group gInfo members <- withFastStore $ \db -> getGroup db vr user groupId
let (count, invitedMems, pendingApprvMems, pendingRvwMems, currentMems, maxRole, anyAdmin) = selectMembers gmIds members
gmIds = S.fromList $ L.toList groupMemberIds
memCount = length groupMemberIds
when (count /= memCount) $ throwChatError CEGroupMemberNotFound
when (memCount > 1 && anyAdmin) $ throwCmdError "can't remove multiple members when admins selected"
assertUserGroupRole gInfo $ max GRAdmin maxRole
(errs1, deleted1) <- deleteInvitedMems user invitedMems
let recipients = filter memberCurrent members
(errs2, deleted2, acis2) <- deleteMemsSend user gInfo Nothing recipients currentMems
(errs3, deleted3, acis3) <-
foldM (\acc m -> deletePendingMember acc user gInfo [m] m) ([], [], []) pendingApprvMems
let moderators = filter (\GroupMember {memberRole} -> memberRole >= GRModerator) members
(errs4, deleted4, acis4) <-
foldM (\acc m -> deletePendingMember acc user gInfo (m : moderators) m) ([], [], []) pendingRvwMems
let acis = acis2 <> acis3 <> acis4
errs = errs1 <> errs2 <> errs3 <> errs4
deleted = deleted1 <> deleted2 <> deleted3 <> deleted4
-- Read group info with updated membersRequireAttention
gInfo' <- withFastStore $ \db -> getGroupInfo db vr user groupId
let acis' = map (updateACIGroupInfo gInfo') acis
unless (null acis') $ toView $ CEvtNewChatItems user acis'
unless (null errs) $ toView $ CEvtChatErrors errs
when withMessages $ deleteMessages user gInfo' deleted
pure $ CRUserDeletedMembers user gInfo' deleted withMessages -- same order is not guaranteed
where
selectMembers :: S.Set GroupMemberId -> [GroupMember] -> (Int, [GroupMember], [GroupMember], [GroupMember], [GroupMember], GroupMemberRole, Bool)
selectMembers gmIds = foldl' addMember (0, [], [], [], [], GRObserver, False)
where
addMember acc@(n, invited, pendingApprv, pendingRvw, current, maxRole, anyAdmin) m@GroupMember {groupMemberId, memberStatus, memberRole}
| groupMemberId `S.member` gmIds =
let maxRole' = max maxRole memberRole
anyAdmin' = anyAdmin || memberRole >= GRAdmin
n' = n + 1
in case memberStatus of
GSMemInvited -> (n', m : invited, pendingApprv, pendingRvw, current, maxRole', anyAdmin')
GSMemPendingApproval -> (n', invited, m : pendingApprv, pendingRvw, current, maxRole', anyAdmin')
GSMemPendingReview -> (n', invited, pendingApprv, m : pendingRvw, current, maxRole', anyAdmin')
_ -> (n', invited, pendingApprv, pendingRvw, m : current, maxRole', anyAdmin')
| otherwise = acc
deleteInvitedMems :: User -> [GroupMember] -> CM ([ChatError], [GroupMember])
deleteInvitedMems user memsToDelete = do
deleteMembersConnections user memsToDelete
lift $ partitionEithers <$> withStoreBatch' (\db -> map (delMember db) memsToDelete)
where
delMember db m = do
deleteGroupMember db user m
pure m {memberStatus = GSMemRemoved}
deletePendingMember :: ([ChatError], [GroupMember], [AChatItem]) -> User -> GroupInfo -> [GroupMember] -> GroupMember -> CM ([ChatError], [GroupMember], [AChatItem])
deletePendingMember (accErrs, accDeleted, accACIs) user gInfo recipients m = do
(m', scopeInfo) <- mkMemberSupportChatInfo m
(errs, deleted, acis) <- deleteMemsSend user gInfo (Just scopeInfo) recipients [m']
pure (errs <> accErrs, deleted <> accDeleted, acis <> accACIs)
deleteMemsSend :: User -> GroupInfo -> Maybe GroupChatScopeInfo -> [GroupMember] -> [GroupMember] -> CM ([ChatError], [GroupMember], [AChatItem])
deleteMemsSend user gInfo chatScopeInfo recipients memsToDelete = case L.nonEmpty memsToDelete of
Nothing -> pure ([], [], [])
Just memsToDelete' -> do
let chatScope = toChatScope <$> chatScopeInfo
events = L.map (\GroupMember {memberId} -> XGrpMemDel memberId withMessages) memsToDelete'
(msgs_, _gsr) <- sendGroupMessages user gInfo chatScope recipients events
let itemsData_ = zipWith (fmap . sndItemData) memsToDelete (L.toList msgs_)
skipUnwantedItem = \case
Right Nothing -> Nothing
Right (Just a) -> Just $ Right a
Left e -> Just $ Left e
itemsData = mapMaybe skipUnwantedItem itemsData_
cis_ <- saveSndChatItems user (CDGroupSnd gInfo chatScopeInfo) itemsData Nothing False
deleteMembersConnections' user memsToDelete True
(errs, deleted) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (delMember db) memsToDelete)
let acis = map (AChatItem SCTGroup SMDSnd (GroupChat gInfo chatScopeInfo)) $ rights cis_
pure (errs, deleted, acis)
where
sndItemData :: GroupMember -> SndMessage -> Maybe (NewSndChatItemData c)
sndItemData GroupMember {groupMemberId, memberProfile, memberStatus} msg
| memberStatus == GSMemRemoved || memberStatus == GSMemLeft = Nothing
| otherwise =
let content = CISndGroupEvent $ SGEMemberDeleted groupMemberId (fromLocalProfile memberProfile)
ts = ciContentTexts content
in Just $ NewSndChatItemData msg content ts M.empty Nothing Nothing Nothing
delMember db m = do
-- We're in a function used in batch member deletion, and since we're passing same gInfo for each member,
-- voided result (updated group info) may have incorrect state of membersRequireAttention.
-- To avoid complicating code by chaining group info updates,
-- instead we re-read it once after deleting all members before response.
void $ deleteOrUpdateMemberRecordIO db user gInfo m
pure m {memberStatus = GSMemRemoved}
deleteMessages user gInfo@GroupInfo {membership} ms
| groupFeatureUserAllowed SGFFullDelete gInfo = deleteGroupMembersCIs user gInfo ms membership
| otherwise = markGroupMembersCIsDeleted user gInfo ms membership
APILeaveGroup groupId -> withUser $ \user@User {userId} -> do
gInfo@GroupInfo {membership} <- withFastStore $ \db -> getGroupInfo db vr user groupId
filesInfo <- withFastStore' $ \db -> getGroupFileInfo db user gInfo
withGroupLock "leaveGroup" groupId $ do
cancelFilesInProgress user filesInfo
(members, recipients) <- getRecipients user gInfo
msg <- sendGroupMessage' user gInfo recipients XGrpLeave
(gInfo', scopeInfo) <- mkLocalGroupChatScope gInfo
ci <- saveSndChatItem user (CDGroupSnd gInfo' scopeInfo) msg (CISndGroupEvent SGEUserLeft)
toView $ CEvtNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo' scopeInfo) ci]
-- TODO delete direct connections that were unused
deleteGroupLinkIfExists user gInfo'
-- member records are not deleted to keep history
deleteMembersConnections' user members True
withFastStore' $ \db -> updateGroupMemberStatus db userId membership GSMemLeft
pure $ CRLeftMemberUser user gInfo' {membership = membership {memberStatus = GSMemLeft}}
where
getRecipients user gInfo@GroupInfo {useRelays}
| isTrue useRelays = do
relays <- withFastStore' $ \db -> getGroupRelays db vr user gInfo
pure (relays, relays)
| otherwise = do
ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo
pure (ms, filter memberCurrentOrPending ms)
APIListMembers groupId -> withUser $ \user ->
CRGroupMembers user <$> withFastStore (\db -> getGroup db vr user groupId)
-- -- validate: prohibit to delete/archive if member is pending (has to communicate approval or rejection)
-- APIDeleteGroupConversations groupId _gcId -> withUser $ \user -> do
-- _gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId
-- ok_ -- CRGroupConversationsArchived
-- APIArchiveGroupConversations groupId _gcId -> withUser $ \user -> do
-- _gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId
-- ok_ -- CRGroupConversationsDeleted
AddMember gName cName memRole -> withUser $ \user -> do
(groupId, contactId) <- withFastStore $ \db -> (,) <$> getGroupIdByName db user gName <*> getContactIdByName db user cName
processChatCommand vr nm $ APIAddMember groupId contactId memRole
JoinGroup gName enableNtfs -> withUser $ \user -> do
groupId <- withFastStore $ \db -> getGroupIdByName db user gName
processChatCommand vr nm $ APIJoinGroup groupId enableNtfs
AcceptMember gName gMemberName memRole -> withMemberName gName gMemberName $ \gId gMemberId -> APIAcceptMember gId gMemberId memRole
MemberRole gName gMemberName memRole -> withMemberName gName gMemberName $ \gId gMemberId -> APIMembersRole gId [gMemberId] memRole
BlockForAll gName gMemberName blocked -> withMemberName gName gMemberName $ \gId gMemberId -> APIBlockMembersForAll gId [gMemberId] blocked
RemoveMembers gName gMemberNames withMessages -> withUser $ \user -> do
(gId, gMemberIds) <- withStore $ \db -> do
gId <- getGroupIdByName db user gName
gMemberIds <- mapM (getGroupMemberIdByName db user gId) gMemberNames
pure (gId, gMemberIds)
processChatCommand vr nm $ APIRemoveMembers gId gMemberIds withMessages
LeaveGroup gName -> withUser $ \user -> do
groupId <- withFastStore $ \db -> getGroupIdByName db user gName
processChatCommand vr nm $ APILeaveGroup groupId
DeleteGroup gName -> withUser $ \user -> do
groupId <- withFastStore $ \db -> getGroupIdByName db user gName
processChatCommand vr nm $ APIDeleteChat (ChatRef CTGroup groupId Nothing) (CDMFull True)
ClearGroup gName -> withUser $ \user -> do
groupId <- withFastStore $ \db -> getGroupIdByName db user gName
processChatCommand vr nm $ APIClearChat (ChatRef CTGroup groupId Nothing)
ListMembers gName -> withUser $ \user -> do
groupId <- withFastStore $ \db -> getGroupIdByName db user gName
processChatCommand vr nm $ APIListMembers groupId
ListMemberSupportChats gName -> withUser $ \user -> do
groupId <- withFastStore $ \db -> getGroupIdByName db user gName
(Group gInfo members) <- withFastStore $ \db -> getGroup db vr user groupId
let memberSupportChats = filter (isJust . supportChat) members
pure $ CRMemberSupportChats user gInfo memberSupportChats
APIListGroups userId contactId_ search_ -> withUserId userId $ \user ->
CRGroupsList user <$> withFastStore' (\db -> getBaseGroupDetails db vr user contactId_ search_)
ListGroups cName_ search_ -> withUser $ \user@User {userId} -> do
ct_ <- forM cName_ $ \cName -> withFastStore $ \db -> getContactByName db vr user cName
processChatCommand vr nm $ APIListGroups userId (contactId' <$> ct_) search_
APIUpdateGroupProfile groupId p' -> withUser $ \user -> do
gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId
runUpdateGroupProfile user gInfo p'
UpdateGroupNames gName GroupProfile {displayName, fullName, shortDescr} ->
updateGroupProfileByName gName $ \p -> p {displayName, fullName, shortDescr}
ShowGroupProfile gName -> withUser $ \user ->
CRGroupProfile user <$> withFastStore (\db -> getGroupInfoByName db vr user gName)
UpdateGroupDescription gName description ->
updateGroupProfileByName gName $ \p -> p {description}
ShowGroupDescription gName -> withUser $ \user ->
CRGroupDescription user <$> withFastStore (\db -> getGroupInfoByName db vr user gName)
APICreateGroupLink groupId mRole -> withUser $ \user -> withGroupLock "createGroupLink" groupId $ do
gInfo@GroupInfo {groupProfile} <- withFastStore $ \db -> getGroupInfo db vr user groupId
assertUserGroupRole gInfo GRAdmin
when (mRole > GRMember) $ throwChatError $ CEGroupMemberInitialRole gInfo mRole
groupLinkId <- GroupLinkId <$> drgRandomBytes 16
subMode <- chatReadVar subscriptionMode
let userData = encodeShortLinkData $ GroupShortLinkData groupProfile
userLinkData = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData}
crClientData = encodeJSON $ CRDataGroup groupLinkId
-- TODO [certs rcv]
(connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True True SCMContact (Just userLinkData) (Just crClientData) IKPQOff subMode
ccLink' <- createdGroupLink <$> shortenCreatedLink ccLink
gVar <- asks random
gLink <- withFastStore $ \db -> createGroupLink db gVar user gInfo connId ccLink' groupLinkId mRole subMode
pure $ CRGroupLinkCreated user gInfo gLink
APIGroupLinkMemberRole groupId mRole' -> withUser $ \user -> withGroupLock "groupLinkMemberRole" groupId $ do
gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId
gLnk@GroupLink {acceptMemberRole} <- withFastStore $ \db -> getGroupLink db user gInfo
assertUserGroupRole gInfo GRAdmin
when (mRole' > GRMember) $ throwChatError $ CEGroupMemberInitialRole gInfo mRole'
gLnk' <-
if mRole' /= acceptMemberRole
then withFastStore' $ \db -> setGroupLinkMemberRole db user gLnk mRole'
else pure gLnk
pure $ CRGroupLink user gInfo gLnk'
APIDeleteGroupLink groupId -> withUser $ \user -> withGroupLock "deleteGroupLink" groupId $ do
gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId
deleteGroupLink' user gInfo
pure $ CRGroupLinkDeleted user gInfo
APIGetGroupLink groupId -> withUser $ \user -> do
gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId
gLnk <- withFastStore $ \db -> getGroupLink db user gInfo
pure $ CRGroupLink user gInfo gLnk
APIAddGroupShortLink groupId -> withUser $ \user -> do
(gInfo, gLink) <- withFastStore $ \db -> do
gInfo <- getGroupInfo db vr user groupId
gLink <- getGroupLink db user gInfo
pure (gInfo, gLink)
gLink' <- setGroupLinkData nm user gInfo gLink
pure $ CRGroupLink user gInfo gLink'
APICreateMemberContact gId gMemberId -> withUser $ \user -> do
(g, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId
assertUserGroupRole g GRAuthor
unless (groupFeatureUserAllowed SGFDirectMessages g) $ throwCmdError "direct messages not allowed"
case memberConn m of
Just mConn@Connection {peerChatVRange} -> do
unless (maxVersion peerChatVRange >= groupDirectInvVersion) $ throwChatError CEPeerChatVRangeIncompatible
when (isJust $ memberContactId m) $ throwCmdError "member contact already exists"
subMode <- chatReadVar subscriptionMode
-- TODO PQ should negotitate contact connection with PQSupportOn?
-- TODO [certs rcv]
(connId, (CCLink cReq _, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True False SCMInvitation Nothing Nothing IKPQOff subMode
-- [incognito] reuse membership incognito profile
ct <- withFastStore' $ \db -> createMemberContact db user connId cReq g m mConn subMode
void $ createChatItem user (CDDirectSnd ct) False CIChatBanner Nothing (Just epochStart)
-- TODO not sure it is correct to set connections status here?
pure $ CRNewMemberContact user ct g m
_ -> throwChatError CEGroupMemberNotActive
APISendMemberContactInvitation contactId msgContent_ -> withUser $ \user -> do
(g@GroupInfo {groupId}, m, ct, cReq) <- withFastStore $ \db -> getMemberContact db vr user contactId
when (contactGrpInvSent ct) $ throwCmdError "x.grp.direct.inv already sent"
case memberConn m of
Just mConn -> do
-- TODO [knocking] send in correct scope - modiy API
let msg = XGrpDirectInv cReq msgContent_ Nothing
(sndMsg, _, _) <- sendDirectMemberMessage mConn msg groupId
withFastStore' $ \db -> setContactGrpInvSent db ct True
let ct' = ct {contactGrpInvSent = True}
forM_ msgContent_ $ \mc -> do
ci <- saveSndChatItem user (CDDirectSnd ct') sndMsg (CISndMsgContent mc)
toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct') ci]
pure $ CRNewMemberContactSentInv user ct' g m
_ -> throwChatError CEGroupMemberNotActive
APIAcceptMemberContact contactId -> withUser $ \user -> do
(g, mConn, ct, groupDirectInv) <- withFastStore $ \db -> getMemberContactInvited db vr user contactId
when (groupDirectInvStartedConnection groupDirectInv) $ throwCmdError "connection already started"
connectMemberContact user g mConn ct groupDirectInv `catchAllErrors` \e -> do
-- get updated contact, in case connection was started
ct' <- withFastStore $ \db -> getContact db vr user contactId
toView $ CEvtChatInfoUpdated user (AChatInfo SCTDirect $ DirectChat ct')
throwError e
-- get updated contact (groupDirectInvStartedConnection) with connection
ct' <- withFastStore $ \db -> do
liftIO $ setMemberContactStartedConnection db ct
getContact db vr user contactId
pure $ CRMemberContactAccepted user ct'
where
connectMemberContact user gInfo mConn Contact {activeConn} GroupDirectInvitation {groupDirectInvLink = cReq} =
withInvitationLock "connect" (strEncode cReq) $ do
subMode <- chatReadVar subscriptionMode
case activeConn of
-- Nothing is legacy branch for exisiting contacts without prepared connection;
-- for new member contacts connection is prepared immediately (on xGrpDirectInv),
-- so incognito profile can be attached to it and be visible in UI before accepting
Nothing -> joinNewConn subMode
Just conn@Connection {connStatus} -> case connStatus of
ConnPrepared -> joinPreparedConn subMode conn
_ -> throwChatError $ CEException "connection already started (past prepared status)"
where
joinNewConn subMode = do
-- possible improvement: use agent connRequestPQSupport to determine pqSupport here;
-- for joinPreparedConn below - same + encodeConnInfoPQ;
-- same for auto-accept on xGrpDirectInv
acId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq PQSupportOff
conn <- withStore $ \db -> do
connId <- liftIO $ createMemberContactConn db user acId Nothing gInfo mConn ConnPrepared contactId subMode
getConnectionById db vr user connId
joinPreparedConn subMode conn
joinPreparedConn subMode conn = do
-- [incognito] send membership incognito profile
let p = userProfileDirect user (fromLocalProfile <$> incognitoMembershipProfile gInfo) Nothing True
dm <- encodeConnInfo $ XInfo p
(sqSecured, _serviceId) <- withAgent $ \a -> joinConnection a nm (aUserId user) (aConnId conn) True cReq dm PQSupportOff subMode
let newStatus = if sqSecured then ConnSndReady else ConnJoined
void $ withFastStore' $ \db -> updateConnectionStatusFromTo db conn ConnPrepared newStatus
CreateGroupLink gName mRole -> withUser $ \user -> do
groupId <- withFastStore $ \db -> getGroupIdByName db user gName
processChatCommand vr nm $ APICreateGroupLink groupId mRole
GroupLinkMemberRole gName mRole -> withUser $ \user -> do
groupId <- withFastStore $ \db -> getGroupIdByName db user gName
processChatCommand vr nm $ APIGroupLinkMemberRole groupId mRole
DeleteGroupLink gName -> withUser $ \user -> do
groupId <- withFastStore $ \db -> getGroupIdByName db user gName
processChatCommand vr nm $ APIDeleteGroupLink groupId
ShowGroupLink gName -> withUser $ \user -> do
groupId <- withFastStore $ \db -> getGroupIdByName db user gName
processChatCommand vr nm $ APIGetGroupLink groupId
SendGroupMessageQuote gName cName quotedMsg msg -> withUser $ \user -> do
(groupId, quotedItemId, mentions) <-
withFastStore $ \db -> do
gId <- getGroupIdByName db user gName
qiId <- getGroupChatItemIdByText db user gId cName quotedMsg
(gId, qiId,) <$> liftIO (getMessageMentions db user gId msg)
let mc = MCText msg
processChatCommand vr nm $ APISendMessages (SRGroup groupId Nothing) False Nothing [ComposedMessage Nothing (Just quotedItemId) mc mentions]
ClearNoteFolder -> withUser $ \user -> do
folderId <- withFastStore (`getUserNoteFolderId` user)
processChatCommand vr nm $ APIClearChat (ChatRef CTLocal folderId Nothing)
LastChats count_ -> withUser' $ \user -> do
let count = fromMaybe 5000 count_
(errs, previews) <- partitionEithers <$> withFastStore' (\db -> getChatPreviews db vr user False (PTLast count) clqNoFilters)
unless (null errs) $ toView $ CEvtChatErrors (map ChatErrorStore errs)
pure $ CRChats previews
LastMessages (Just chatName) count search -> withUser $ \user -> do
chatRef <- getChatRef user chatName
chatResp <- processChatCommand vr nm $ APIGetChat chatRef Nothing (CPLast count) search
pure $ CRChatItems user (Just chatName) (aChatItems . chat $ chatResp)
LastMessages Nothing count search -> withUser $ \user -> do
chatItems <- withFastStore $ \db -> getAllChatItems db vr user (CPLast count) search
pure $ CRChatItems user Nothing chatItems
LastChatItemId (Just chatName) index -> withUser $ \user -> do
chatRef <- getChatRef user chatName
chatResp <- processChatCommand vr nm $ APIGetChat chatRef Nothing (CPLast $ index + 1) Nothing
pure $ CRChatItemId user (fmap aChatItemId . listToMaybe . aChatItems . chat $ chatResp)
LastChatItemId Nothing index -> withUser $ \user -> do
chatItems <- withFastStore $ \db -> getAllChatItems db vr user (CPLast $ index + 1) Nothing
pure $ CRChatItemId user (fmap aChatItemId . listToMaybe $ chatItems)
ShowChatItem (Just itemId) -> withUser $ \user -> do
chatItem <- withFastStore $ \db -> do
chatRef <- getChatRefViaItemId db user itemId
getAChatItem db vr user chatRef itemId
pure $ CRChatItems user Nothing ((: []) chatItem)
ShowChatItem Nothing -> withUser $ \user -> do
chatItems <- withFastStore $ \db -> getAllChatItems db vr user (CPLast 1) Nothing
pure $ CRChatItems user Nothing chatItems
ShowChatItemInfo chatName msg -> withUser $ \user -> do
chatRef <- getChatRef user chatName
itemId <- getChatItemIdByText user chatRef msg
processChatCommand vr nm $ APIGetChatItemInfo chatRef itemId
ShowLiveItems on -> withUser $ \_ ->
asks showLiveItems >>= atomically . (`writeTVar` on) >> ok_
SendFile chatName f -> withUser $ \user -> do
chatRef <- getChatRef user chatName
case chatRef of
ChatRef CTLocal folderId _ -> processChatCommand vr nm $ APICreateChatItems folderId [composedMessage (Just f) (MCFile "")]
_ -> withSendRef chatRef $ \sendRef -> processChatCommand vr nm $ APISendMessages sendRef False Nothing [composedMessage (Just f) (MCFile "")]
SendImage chatName f@(CryptoFile fPath _) -> withUser $ \user -> do
chatRef <- getChatRef user chatName
withSendRef chatRef $ \sendRef -> do
filePath <- lift $ toFSFilePath fPath
unless (any (`isSuffixOf` map toLower fPath) imageExtensions) $ throwChatError CEFileImageType {filePath}
fileSize <- getFileSize filePath
unless (fileSize <= maxImageSize) $ throwChatError CEFileImageSize {filePath}
-- TODO include file description for preview
processChatCommand vr nm $ APISendMessages sendRef False Nothing [composedMessage (Just f) (MCImage "" fixedImagePreview)]
ForwardFile chatName fileId -> forwardFile chatName fileId SendFile
ForwardImage chatName fileId -> forwardFile chatName fileId SendImage
SendFileDescription _chatName _f -> throwCmdError "TODO"
-- TODO to use priority transactions we need a parameter that differentiates manual and automatic acceptance
ReceiveFile fileId userApprovedRelays encrypted_ rcvInline_ filePath_ -> withUser $ \_ ->
withFileLock "receiveFile" fileId $ do
(user, ft@RcvFileTransfer {fileStatus}) <- withStore (`getRcvFileTransferById` fileId)
encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles
ft' <- (if encrypt && fileStatus == RFSNew then setFileToEncrypt else pure) ft
receiveFile' user ft' userApprovedRelays rcvInline_ filePath_
SetFileToReceive fileId userApprovedRelays encrypted_ -> withUser $ \_ -> do
withFileLock "setFileToReceive" fileId $ do
encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles
cfArgs <- if encrypt then Just <$> (atomically . CF.randomArgs =<< asks random) else pure Nothing
withStore' $ \db -> setRcvFileToReceive db fileId userApprovedRelays cfArgs
ok_
CancelFile fileId -> withUser $ \user@User {userId} ->
withFileLock "cancelFile" fileId $
withFastStore (\db -> getFileTransfer db user fileId) >>= \case
FTSnd ftm@FileTransferMeta {xftpSndFile, cancelled} fts
| cancelled -> throwChatError $ CEFileCancel fileId "file already cancelled"
| not (null fts) && all fileCancelledOrCompleteSMP fts ->
throwChatError $ CEFileCancel fileId "file transfer is complete"
| otherwise -> do
cancelSndFile user ftm fts True
cref_ <- withFastStore' $ \db -> lookupChatRefByFileId db user fileId
aci_ <- withFastStore $ \db -> lookupChatItemByFileId db vr user fileId
case (cref_, aci_) of
(Nothing, _) ->
pure $ CRSndFileCancelled user Nothing ftm fts
(Just (ChatRef CTDirect contactId _), Just aci) -> do
(contact, sharedMsgId) <- withFastStore $ \db -> (,) <$> getContact db vr user contactId <*> getSharedMsgIdByFileId db userId fileId
void . sendDirectContactMessage user contact $ XFileCancel sharedMsgId
pure $ CRSndFileCancelled user (Just aci) ftm fts
(Just (ChatRef CTGroup groupId scope), Just aci) -> do
(gInfo, sharedMsgId) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user groupId <*> getSharedMsgIdByFileId db userId fileId
chatScopeInfo <- mapM (getChatScopeInfo vr user) scope
recipients <- getGroupRecipients vr user gInfo chatScopeInfo groupKnockingVersion
void . sendGroupMessage user gInfo scope recipients $ XFileCancel sharedMsgId
pure $ CRSndFileCancelled user (Just aci) ftm fts
(Just _, _) -> throwChatError $ CEFileInternal "invalid chat ref for file transfer"
where
fileCancelledOrCompleteSMP SndFileTransfer {fileStatus = s} =
s == FSCancelled || (s == FSComplete && isNothing xftpSndFile)
FTRcv ftr@RcvFileTransfer {cancelled, fileStatus, xftpRcvFile}
| cancelled -> throwChatError $ CEFileCancel fileId "file already cancelled"
| rcvFileComplete fileStatus -> throwChatError $ CEFileCancel fileId "file transfer is complete"
| otherwise -> case xftpRcvFile of
Nothing -> do
cancelRcvFileTransfer user ftr
ci <- withFastStore $ \db -> lookupChatItemByFileId db vr user fileId
pure $ CRRcvFileCancelled user ci ftr
Just XFTPRcvFile {agentRcvFileId} -> do
forM_ (liveRcvFileTransferPath ftr) $ \filePath -> do
fsFilePath <- lift $ toFSFilePath filePath
liftIO $ removeFile fsFilePath `catchAll_` pure ()
lift . forM_ agentRcvFileId $ \(AgentRcvFileId aFileId) ->
withAgent' (`xftpDeleteRcvFile` aFileId)
aci_ <- resetRcvCIFileStatus user fileId CIFSRcvInvitation
pure $ CRRcvFileCancelled user aci_ ftr
FileStatus fileId -> withUser $ \user -> do
withFastStore (\db -> lookupChatItemByFileId db vr user fileId) >>= \case
Nothing -> do
fileStatus <- withFastStore $ \db -> getFileTransferProgress db user fileId
pure $ CRFileTransferStatus user fileStatus
Just ci@(AChatItem _ _ _ ChatItem {file}) -> case file of
Just CIFile {fileProtocol = FPLocal} ->
throwCmdError "not supported for local files"
Just CIFile {fileProtocol = FPXFTP} ->
pure $ CRFileTransferStatusXFTP user ci
_ -> do
fileStatus <- withFastStore $ \db -> getFileTransferProgress db user fileId
pure $ CRFileTransferStatus user fileStatus
ShowProfile -> withUser $ \user@User {profile} -> pure $ CRUserProfile user (fromLocalProfile profile)
SetBotCommands commands -> withUser $ \user@User {profile} -> do
let LocalProfile {preferences} = profile
prefs = Just (fromMaybe emptyChatPrefs preferences :: Preferences) {commands = Just commands}
p = (fromLocalProfile profile :: Profile) {preferences = prefs, peerType = Just CPTBot}
updateProfile user p
UpdateProfile displayName shortDescr -> withUser $ \user@User {profile} -> do
let p = (fromLocalProfile profile :: Profile) {displayName, shortDescr, fullName = ""}
updateProfile user p
UpdateProfileImage image -> withUser $ \user@User {profile} -> do
let p = (fromLocalProfile profile :: Profile) {image}
updateProfile user p
ShowProfileImage -> withUser $ \user@User {profile} -> pure $ CRUserProfileImage user $ fromLocalProfile profile
SetUserFeature (ACF f) allowed -> withUser $ \user@User {profile} -> do
let p = (fromLocalProfile profile :: Profile) {preferences = Just . setPreference f (Just allowed) $ preferences' user}
updateProfile user p
SetContactFeature (ACF f) cName allowed_ -> withUser $ \user -> do
ct@Contact {userPreferences} <- withFastStore $ \db -> getContactByName db vr user cName
let prefs' = setPreference f allowed_ $ Just userPreferences
updateContactPrefs user ct prefs'
SetGroupFeature (AGFNR f) gName enabled ->
updateGroupProfileByName gName $ \p ->
p {groupPreferences = Just . setGroupPreference f enabled $ groupPreferences p}
SetGroupFeatureRole (AGFR f) gName enabled role ->
updateGroupProfileByName gName $ \p ->
p {groupPreferences = Just . setGroupPreferenceRole f enabled role $ groupPreferences p}
SetGroupMemberAdmissionReview gName reviewAdmissionApplication ->
updateGroupProfileByName gName $ \p@GroupProfile {memberAdmission} ->
case memberAdmission of
Nothing -> p {memberAdmission = Just (emptyGroupMemberAdmission :: GroupMemberAdmission) {review = reviewAdmissionApplication}}
Just ma -> p {memberAdmission = Just (ma :: GroupMemberAdmission) {review = reviewAdmissionApplication}}
SetUserTimedMessages onOff -> withUser $ \user@User {profile} -> do
let allowed = if onOff then FAYes else FANo
pref = TimedMessagesPreference allowed Nothing
p = (fromLocalProfile profile :: Profile) {preferences = Just . setPreference' SCFTimedMessages (Just pref) $ preferences' user}
updateProfile user p
SetContactTimedMessages cName timedMessagesEnabled_ -> withUser $ \user -> do
ct@Contact {userPreferences = userPreferences@Preferences {timedMessages}} <- withFastStore $ \db -> getContactByName db vr user cName
let currentTTL = timedMessages >>= \TimedMessagesPreference {ttl} -> ttl
pref_ = tmeToPref currentTTL <$> timedMessagesEnabled_
prefs' = setPreference' SCFTimedMessages pref_ $ Just userPreferences
updateContactPrefs user ct prefs'
SetGroupTimedMessages gName ttl_ -> do
let pref = uncurry TimedMessagesGroupPreference $ maybe (FEOff, Just 86400) (\ttl -> (FEOn, Just ttl)) ttl_
updateGroupProfileByName gName $ \p ->
p {groupPreferences = Just . setGroupPreference' SGFTimedMessages pref $ groupPreferences p}
SetLocalDeviceName name -> chatWriteVar localDeviceName name >> ok_
ListRemoteHosts -> CRRemoteHostList <$> listRemoteHosts
SwitchRemoteHost rh_ -> CRCurrentRemoteHost <$> switchRemoteHost rh_
StartRemoteHost rh_ ca_ bp_ -> do
(localAddrs, remoteHost_, inv@RCSignedInvitation {invitation = RCInvitation {port}}) <- startRemoteHost rh_ ca_ bp_
pure CRRemoteHostStarted {remoteHost_, invitation = decodeLatin1 $ strEncode inv, ctrlPort = show port, localAddrs}
StopRemoteHost rh_ -> closeRemoteHost rh_ >> ok_
DeleteRemoteHost rh -> deleteRemoteHost rh >> ok_
StoreRemoteFile rh encrypted_ localPath -> CRRemoteFileStored rh <$> storeRemoteFile rh encrypted_ localPath
GetRemoteFile rh rf -> getRemoteFile rh rf >> ok_
ConnectRemoteCtrl inv -> withUser_ $ do
(remoteCtrl_, ctrlAppInfo) <- connectRemoteCtrlURI inv
pure CRRemoteCtrlConnecting {remoteCtrl_, ctrlAppInfo, appVersion = currentAppVersion}
FindKnownRemoteCtrl -> withUser_ $ findKnownRemoteCtrl >> ok_
ConfirmRemoteCtrl rcId -> withUser_ $ do
(rc, ctrlAppInfo) <- confirmRemoteCtrl rcId
pure CRRemoteCtrlConnecting {remoteCtrl_ = Just rc, ctrlAppInfo, appVersion = currentAppVersion}
VerifyRemoteCtrlSession sessId -> withUser_ $ verifyRemoteCtrlSession (execChatCommand Nothing) sessId
StopRemoteCtrl -> withUser_ $ stopRemoteCtrl >> ok_
ListRemoteCtrls -> withUser_ $ CRRemoteCtrlList <$> listRemoteCtrls
DeleteRemoteCtrl rc -> withUser_ $ deleteRemoteCtrl rc >> ok_
APIUploadStandaloneFile userId file@CryptoFile {filePath} -> withUserId userId $ \user -> do
fsFilePath <- lift $ toFSFilePath filePath
fileSize <- liftIO $ CF.getFileContentsSize file {filePath = fsFilePath}
when (fileSize > toInteger maxFileSizeHard) $ throwChatError $ CEFileSize filePath
(_, _, fileTransferMeta) <- xftpSndFileTransfer_ user file fileSize 1 Nothing
pure CRSndStandaloneFileCreated {user, fileTransferMeta}
APIStandaloneFileInfo FileDescriptionURI {clientData} -> pure . CRStandaloneFileInfo $ clientData >>= J.decodeStrict . encodeUtf8
APIDownloadStandaloneFile userId uri file -> withUserId userId $ \user -> do
ft <- receiveViaURI user uri file
pure $ CRRcvStandaloneFileCreated user ft
QuitChat -> liftIO exitSuccess
ShowVersion -> do
-- simplexmqCommitQ makes iOS builds crash m(
let versionInfo = coreVersionInfo ""
chatMigrations <- map upMigration <$> withFastStore' (getCurrentMigrations Nothing)
agentMigrations <- withAgent getAgentMigrations
pure $ CRVersionInfo {versionInfo, chatMigrations, agentMigrations}
DebugLocks -> lift $ do
chatLockName <- atomically . tryReadTMVar =<< asks chatLock
chatEntityLocks <- getLocks =<< asks entityLocks
agentLocks <- withAgent' debugAgentLocks
pure CRDebugLocks {chatLockName, chatEntityLocks, agentLocks}
where
getLocks ls = atomically $ M.mapKeys enityLockString . M.mapMaybe id <$> (mapM tryReadTMVar =<< readTVar ls)
enityLockString cle = case cle of
CLInvitation bs -> "Invitation " <> safeDecodeUtf8 bs
CLConnection connId -> "Connection " <> tshow connId
CLContact ctId -> "Contact " <> tshow ctId
CLGroup gId -> "Group " <> tshow gId
CLUserContact ucId -> "UserContact " <> tshow ucId
CLContactRequest crId -> "ContactRequest " <> tshow crId
CLFile fId -> "File " <> tshow fId
DebugEvent event -> toView event >> ok_
GetAgentSubsTotal userId -> withUserId userId $ \user -> do
users <- withStore' $ \db -> getUsers db
let userIds = map aUserId $ filter (\u -> isNothing (viewPwdHash u) || aUserId u == aUserId user) users
(subsTotal, hasSession) <- lift $ withAgent' $ \a -> getAgentSubsTotal a userIds
pure $ CRAgentSubsTotal user subsTotal hasSession
GetAgentServersSummary userId -> withUserId userId $ \user -> do
agentServersSummary <- lift $ withAgent' getAgentServersSummary
withStore' $ \db -> do
users <- getUsers db
smpServers <- getServers db user SPSMP
xftpServers <- getServers db user SPXFTP
let presentedServersSummary = toPresentedServersSummary agentServersSummary users user smpServers xftpServers _defaultNtfServers
pure $ CRAgentServersSummary user presentedServersSummary
where
getServers :: ProtocolTypeI p => DB.Connection -> User -> SProtocolType p -> IO [ProtocolServer p]
getServers db user p = map (\UserServer {server} -> protoServer server) <$> getProtocolServers db p user
ResetAgentServersStats -> withAgent resetAgentServersStats >> ok_
GetAgentWorkers -> lift $ CRAgentWorkersSummary <$> withAgent' getAgentWorkersSummary
GetAgentWorkersDetails -> lift $ CRAgentWorkersDetails <$> withAgent' getAgentWorkersDetails
GetAgentSubs -> lift $ summary <$> withAgent' getAgentSubscriptions
where
summary SubscriptionsInfo {activeSubscriptions, pendingSubscriptions, removedSubscriptions} =
CRAgentSubs
{ activeSubs = foldl' countSubs M.empty activeSubscriptions,
pendingSubs = foldl' countSubs M.empty pendingSubscriptions,
removedSubs = foldl' accSubErrors M.empty removedSubscriptions
}
where
countSubs m SubInfo {server} = M.alter (Just . maybe 1 (+ 1)) server m
accSubErrors m = \case
SubInfo {server, subError = Just e} -> M.alter (Just . maybe [e] (e :)) server m
_ -> m
GetAgentSubsDetails -> lift $ CRAgentSubsDetails <$> withAgent' getAgentSubscriptions
GetAgentQueuesInfo -> lift $ CRAgentQueuesInfo <$> withAgent' getAgentQueuesInfo
-- CustomChatCommand is unsupported, it can be processed in preCmdHook
-- in a modified CLI app or core - the hook should return Either (Either ChatError ChatResponse) ChatCommand,
-- where Left means command result, and Right some other command to be processed by this function.
CustomChatCommand _cmd -> withUser $ \_ -> throwCmdError "not supported"
where
ok_ = pure $ CRCmdOk Nothing
ok = pure . CRCmdOk . Just
getChatRef :: User -> ChatName -> CM ChatRef
getChatRef user (ChatName cType name) = do
chatId <- case cType of
CTDirect -> withFastStore $ \db -> getContactIdByName db user name
CTGroup -> withFastStore $ \db -> getGroupIdByName db user name
CTLocal
| name == "" -> withFastStore (`getUserNoteFolderId` user)
| otherwise -> throwCmdError "not supported"
_ -> throwCmdError "not supported"
pure $ ChatRef cType chatId Nothing
getChatRefAndMentions :: User -> ChatName -> Text -> CM (ChatRef, Map MemberName GroupMemberId)
getChatRefAndMentions user cName msg = do
chatRef@(ChatRef cType chatId _) <- getChatRef user cName
(chatRef,) <$> case cType of
CTGroup -> withFastStore' $ \db -> getMessageMentions db user chatId msg
_ -> pure []
#if !defined(dbPostgres)
checkChatStopped :: CM ChatResponse -> CM ChatResponse
checkChatStopped a = asks agentAsync >>= readTVarIO >>= maybe a (const $ throwChatError CEChatNotStopped)
setStoreChanged :: CM ()
setStoreChanged = asks chatStoreChanged >>= atomically . (`writeTVar` True)
withStoreChanged :: CM () -> CM ChatResponse
withStoreChanged a = checkChatStopped $ a >> setStoreChanged >> ok_
#endif
checkStoreNotChanged :: CM ChatResponse -> CM ChatResponse
checkStoreNotChanged = ifM (asks chatStoreChanged >>= readTVarIO) (throwChatError CEChatStoreChanged)
withUserName :: UserName -> (UserId -> ChatCommand) -> CM ChatResponse
withUserName uName cmd = withFastStore (`getUserIdByName` uName) >>= processChatCommand vr nm . cmd
withContactName :: ContactName -> (ContactId -> ChatCommand) -> CM ChatResponse
withContactName cName cmd = withUser $ \user ->
withFastStore (\db -> getContactIdByName db user cName) >>= processChatCommand vr nm . cmd
withMemberName :: GroupName -> ContactName -> (GroupId -> GroupMemberId -> ChatCommand) -> CM ChatResponse
withMemberName gName mName cmd = withUser $ \user ->
getGroupAndMemberId user gName mName >>= processChatCommand vr nm . uncurry cmd
getConnectionCode :: ConnId -> CM Text
getConnectionCode connId = verificationCode <$> withAgent (`getConnectionRatchetAdHash` connId)
verifyConnectionCode :: User -> Connection -> Maybe Text -> CM ChatResponse
verifyConnectionCode user conn@Connection {connId} (Just code) = do
code' <- getConnectionCode $ aConnId conn
let verified = sameVerificationCode code code'
when verified . withFastStore' $ \db -> setConnectionVerified db user connId $ Just code'
pure $ CRConnectionVerified user verified code'
verifyConnectionCode user conn@Connection {connId} _ = do
code' <- getConnectionCode $ aConnId conn
withFastStore' $ \db -> setConnectionVerified db user connId Nothing
pure $ CRConnectionVerified user False code'
getSentChatItemIdByText :: User -> ChatRef -> Text -> CM Int64
getSentChatItemIdByText user@User {userId, localDisplayName} (ChatRef cType cId _scope) msg = case cType of
CTDirect -> withFastStore $ \db -> getDirectChatItemIdByText db userId cId SMDSnd msg
CTGroup -> withFastStore $ \db -> getGroupChatItemIdByText db user cId (Just localDisplayName) msg
CTLocal -> withFastStore $ \db -> getLocalChatItemIdByText db user cId SMDSnd msg
_ -> throwCmdError "not supported"
getChatItemIdByText :: User -> ChatRef -> Text -> CM Int64
getChatItemIdByText user (ChatRef cType cId _scope) msg = case cType of
CTDirect -> withFastStore $ \db -> getDirectChatItemIdByText' db user cId msg
CTGroup -> withFastStore $ \db -> getGroupChatItemIdByText' db user cId msg
CTLocal -> withFastStore $ \db -> getLocalChatItemIdByText' db user cId msg
_ -> throwCmdError "not supported"
connectViaInvitation :: User -> IncognitoEnabled -> CreatedLinkInvitation -> Maybe ContactId -> CM (Connection, Maybe Profile)
connectViaInvitation user@User {userId} incognito (CCLink cReq@(CRInvitationUri crData e2e) sLnk_) contactId_ =
withInvitationLock "connect" (strEncode cReq) $ do
subMode <- chatReadVar subscriptionMode
lift (withAgent' $ \a -> connRequestPQSupport a PQSupportOn cReq) >>= \case
Nothing -> throwChatError CEInvalidConnReq
-- TODO PQ the error above should be CEIncompatibleConnReqVersion, also the same API should be called in Plan
Just (agentV, pqSup') -> do
let chatV = agentToChatVersion agentV
withFastStore' (\db -> getConnectionEntityByConnReq db vr user cReqs) >>= \case
Nothing -> joinNewConn chatV
Just (RcvDirectMsgConnection conn@Connection {connStatus, contactConnInitiated, customUserProfileId} _ct_)
| connStatus == ConnNew && contactConnInitiated -> joinNewConn chatV -- own connection link
| connStatus == ConnPrepared -> do -- retrying join after error
localIncognitoProfile <- forM customUserProfileId $ \pId -> withFastStore $ \db -> getProfileById db userId pId
joinPreparedConn conn (fromLocalProfile <$> localIncognitoProfile) chatV
Just ent -> throwCmdError $ "connection is not RcvDirectMsgConnection: " <> show (connEntityInfo ent)
where
joinNewConn chatV = do
-- [incognito] generate profile to send
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup'
let ccLink = CCLink cReq $ serverShortLink <$> sLnk_
conn <- withFastStore' $ \db -> createDirectConnection' db userId connId ccLink contactId_ ConnPrepared incognitoProfile subMode chatV pqSup'
joinPreparedConn conn incognitoProfile chatV
joinPreparedConn conn incognitoProfile chatV = do
let profileToSend = userProfileDirect user incognitoProfile Nothing True
dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend
(sqSecured, _serviceId) <- withAgent $ \a -> joinConnection a nm (aUserId user) (aConnId conn) True cReq dm pqSup' subMode
let newStatus = if sqSecured then ConnSndReady else ConnJoined
conn' <- withFastStore' $ \db -> updateConnectionStatusFromTo db conn ConnPrepared newStatus
pure (conn', incognitoProfile)
cReqs =
( CRInvitationUri crData {crScheme = SSSimplex} e2e,
CRInvitationUri crData {crScheme = simplexChat} e2e
)
connectViaContact :: User -> Maybe PreparedChatEntity -> IncognitoEnabled -> CreatedLinkContact -> Maybe SharedMsgId -> Maybe (SharedMsgId, MsgContent) -> CM ConnectViaContactResult
connectViaContact user@User {userId} preparedEntity_ incognito (CCLink cReq@(CRContactUri crData@ConnReqUriData {crClientData}) sLnk) welcomeSharedMsgId msg_ = withInvitationLock "connectViaContact" (strEncode cReq) $ do
let groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli
-- groupLinkId is Nothing for business chats
when (isJust msg_ && isJust groupLinkId) $ throwChatError CEConnReqMessageProhibited
case preparedEntity_ of
Just (PCEContact ct@Contact {activeConn}) -> case activeConn of
Nothing -> connect' Nothing Nothing
Just conn@Connection {connStatus, xContactId} -> case connStatus of
ConnPrepared -> joinPreparedConn' xContactId conn Nothing
_ -> pure $ CVRConnectedContact ct
Just (PCEGroup gInfo GroupMember {activeConn}) -> case activeConn of
Nothing -> connect' groupLinkId Nothing
Just conn@Connection {connStatus, xContactId} -> case connStatus of
ConnPrepared -> joinPreparedConn' xContactId conn $ Just (Just gInfo)
_ -> connect' groupLinkId xContactId -- why not "already connected" for host member?
Nothing ->
withFastStore' (\db -> getConnReqContactXContactId db vr user cReqHash1 cReqHash2) >>= \case
Right ct@Contact {activeConn} -> case groupLinkId of
Nothing -> case activeConn of
Just conn@Connection {connStatus = ConnPrepared, xContactId} -> joinPreparedConn' xContactId conn Nothing
_ -> pure $ CVRConnectedContact ct
Just gLinkId ->
-- allow repeat contact request
-- TODO [short links] is this branch needed? it probably remained from the time we created host contact
connect' (Just gLinkId) Nothing
Left conn_ -> case conn_ of
Just conn@Connection {connStatus = ConnPrepared, xContactId} -> joinPreparedConn' xContactId conn $ groupLinkId $> Nothing
-- TODO [short links] this is executed on repeat request after success
-- it probably should send the second message without creating the second connection?
Just Connection {xContactId} -> connect' groupLinkId xContactId
Nothing -> connect' groupLinkId Nothing
where
cReqHash = ConnReqUriHash . C.sha256Hash . strEncode
cReqHash1 = cReqHash $ CRContactUri crData {crScheme = SSSimplex}
cReqHash2 = cReqHash $ CRContactUri crData {crScheme = simplexChat}
joinPreparedConn' xContactId_ conn@Connection {customUserProfileId} gInfo_ = do
when (incognito /= isJust customUserProfileId) $ throwCmdError "incognito mode is different from prepared connection"
xContactId <- mkXContactId xContactId_
localIncognitoProfile <- forM customUserProfileId $ \pId -> withFastStore $ \db -> getProfileById db userId pId
let incognitoProfile = fromLocalProfile <$> localIncognitoProfile
conn' <- joinContact user conn cReq incognitoProfile xContactId welcomeSharedMsgId msg_ gInfo_ PQSupportOn
pure $ CVRSentInvitation conn' incognitoProfile
connect' groupLinkId xContactId_ = do
let inGroup = isJust groupLinkId
pqSup = if inGroup then PQSupportOff else PQSupportOn
(connId, chatV) <- prepareContact user cReq pqSup
xContactId <- mkXContactId xContactId_
-- [incognito] generate profile to send
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
subMode <- chatReadVar subscriptionMode
let sLnk' = serverShortLink <$> sLnk
conn <- withFastStore' $ \db -> createConnReqConnection db userId connId preparedEntity_ cReq cReqHash1 sLnk' xContactId incognitoProfile groupLinkId subMode chatV pqSup
conn' <- joinContact user conn cReq incognitoProfile xContactId welcomeSharedMsgId msg_ (groupLinkId $> Nothing) pqSup
pure $ CVRSentInvitation conn' incognitoProfile
connectContactViaAddress :: User -> IncognitoEnabled -> Contact -> CreatedLinkContact -> CM ChatResponse
connectContactViaAddress user@User {userId} incognito ct@Contact {contactId, activeConn} (CCLink cReq shortLink) =
withInvitationLock "connectContactViaAddress" (strEncode cReq) $
case activeConn of
Nothing -> do
let pqSup = PQSupportOn
(connId, chatV) <- prepareContact user cReq pqSup
newXContactId <- XContactId <$> drgRandomBytes 16
-- [incognito] generate profile to send
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
subMode <- chatReadVar subscriptionMode
let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq
conn <- withFastStore' $ \db -> createConnReqConnection db userId connId (Just $ PCEContact ct) cReq cReqHash shortLink newXContactId incognitoProfile Nothing subMode chatV pqSup
void $ joinContact user conn cReq incognitoProfile newXContactId Nothing Nothing Nothing pqSup
ct' <- withStore $ \db -> getContact db vr user contactId
pure $ CRSentInvitationToContact user ct' incognitoProfile
Just conn@Connection {connStatus, xContactId = xContactId_, customUserProfileId} -> case connStatus of
ConnPrepared -> do
when (incognito /= isJust customUserProfileId) $ throwCmdError "incognito mode is different from prepared connection"
xContactId <- mkXContactId xContactId_
localIncognitoProfile <- forM customUserProfileId $ \pId -> withFastStore $ \db -> getProfileById db userId pId
let incognitoProfile = fromLocalProfile <$> localIncognitoProfile
void $ joinContact user conn cReq incognitoProfile xContactId Nothing Nothing Nothing PQSupportOn
ct' <- withStore $ \db -> getContact db vr user contactId
pure $ CRSentInvitationToContact user ct' incognitoProfile
_ -> throwCmdError "contact already has connection"
prepareContact :: User -> ConnReqContact -> PQSupport -> CM (ConnId, VersionChat)
prepareContact user cReq pqSup = do
-- 0) toggle disabled - PQSupportOff
-- 1) toggle enabled, address supports PQ (connRequestPQSupport returns Just True) - PQSupportOn, enable support with compression
-- 2) toggle enabled, address doesn't support PQ - PQSupportOn but without compression, with version range indicating support
lift (withAgent' $ \a -> connRequestPQSupport a pqSup cReq) >>= \case
Nothing -> throwChatError CEInvalidConnReq
Just (agentV, _) -> do
let chatV = agentToChatVersion agentV
connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup
pure (connId, chatV)
mkXContactId :: Maybe XContactId -> CM XContactId
mkXContactId = maybe (XContactId <$> drgRandomBytes 16) pure
joinContact :: User -> Connection -> ConnReqContact -> Maybe Profile -> XContactId -> Maybe SharedMsgId -> Maybe (SharedMsgId, MsgContent) -> Maybe (Maybe GroupInfo) -> PQSupport -> CM Connection
joinContact user conn@Connection {connChatVersion = chatV} cReq incognitoProfile xContactId welcomeSharedMsgId msg_ gInfo_ pqSup = do
-- gInfo_ is Maybe (Maybe GroupInfo), where Just Nothing means "some unknown group", e.g. when joining via link without profile
let profileToSend = case gInfo_ of
Just gInfo_' ->
let allowSimplexLinks = maybe True (groupFeatureUserAllowed SGFSimplexLinks) gInfo_'
in userProfileInGroup' user allowSimplexLinks incognitoProfile
Nothing -> userProfileDirect user incognitoProfile Nothing True
dm <- encodeConnInfoPQ pqSup chatV (XContact profileToSend (Just xContactId) welcomeSharedMsgId msg_)
subMode <- chatReadVar subscriptionMode
void $ withAgent $ \a -> joinConnection a nm (aUserId user) (aConnId conn) True cReq dm pqSup subMode
withFastStore' $ \db -> updateConnectionStatusFromTo db conn ConnPrepared ConnJoined
contactMember :: Contact -> [GroupMember] -> Maybe GroupMember
contactMember Contact {contactId} =
find $ \GroupMember {memberContactId = cId, memberStatus = s} ->
cId == Just contactId && s /= GSMemRejected && s /= GSMemRemoved && s /= GSMemLeft
checkSndFile :: CryptoFile -> CM Integer
checkSndFile (CryptoFile f cfArgs) = do
fsFilePath <- lift $ toFSFilePath f
unlessM (doesFileExist fsFilePath) . throwChatError $ CEFileNotFound f
fileSize <- liftIO $ CF.getFileContentsSize $ CryptoFile fsFilePath cfArgs
when (fromInteger fileSize > maxFileSize) $ throwChatError $ CEFileSize f
pure fileSize
updateProfile :: User -> Profile -> CM ChatResponse
updateProfile user p' = updateProfile_ user p' True $ withFastStore $ \db -> updateUserProfile db user p'
updateProfile_ :: User -> Profile -> Bool -> CM User -> CM ChatResponse
updateProfile_ user@User {profile = p@LocalProfile {displayName = n}} p'@Profile {displayName = n'} shouldUpdateAddressData updateUser
| p' == fromLocalProfile p = pure $ CRUserProfileNoChange user
| otherwise = do
when (n /= n') $ checkValidName n'
-- read contacts before user update to correctly merge preferences
contacts <- withFastStore' $ \db -> getUserContacts db vr user
user' <- updateUser
asks currentUser >>= atomically . (`writeTVar` Just user')
withChatLock "updateProfile" $ do
when shouldUpdateAddressData $ setMyAddressData' user'
summary <- sendUpdateToContacts user' contacts
pure $ CRUserProfileUpdated user' (fromLocalProfile p) p' summary
where
setMyAddressData' :: User -> CM ()
setMyAddressData' user' =
withFastStore' (\db -> runExceptT $ getUserAddress db user) >>= \case
Right ucl@UserContactLink {shortLinkDataSet}
| shortLinkDataSet -> void $ setMyAddressData user' ucl
_ -> pure ()
sendUpdateToContacts :: User -> [Contact] -> CM UserProfileUpdateSummary
sendUpdateToContacts user' contacts = do
let changedCts_ = L.nonEmpty $ foldr addChangedProfileContact [] contacts
case changedCts_ of
Nothing -> pure $ UserProfileUpdateSummary 0 0 []
Just changedCts -> do
let idsEvts = L.map ctSndEvent changedCts
msgReqs_ <- lift $ L.zipWith ctMsgReq changedCts <$> createSndMessages idsEvts
(errs, cts) <- partitionEithers . L.toList . L.zipWith (second . const) changedCts <$> deliverMessagesB msgReqs_
unless (null errs) $ toView $ CEvtChatErrors errs
let changedCts' = filter (\ChangedProfileContact {ct, ct'} -> directOrUsed ct' && mergedPreferences ct' /= mergedPreferences ct) cts
lift $ createContactsSndFeatureItems user' changedCts'
pure
UserProfileUpdateSummary
{ updateSuccesses = length cts,
updateFailures = length errs,
changedContacts = map (\ChangedProfileContact {ct'} -> ct') changedCts'
}
where
-- [incognito] filter out contacts with whom user has incognito connections
addChangedProfileContact :: Contact -> [ChangedProfileContact] -> [ChangedProfileContact]
addChangedProfileContact ct changedCts = case contactSendConn_ ct' of
Right conn
| not (connIncognito conn) && mergedProfile' /= mergedProfile ->
ChangedProfileContact ct ct' mergedProfile' conn : changedCts
_ -> changedCts
where
mergedProfile = userProfileDirect user Nothing (Just ct) False
ct' = updateMergedPreferences user' ct
mergedProfile' = userProfileDirect user' Nothing (Just ct') False
ctSndEvent :: ChangedProfileContact -> (ConnOrGroupId, ChatMsgEvent 'Json)
ctSndEvent ChangedProfileContact {mergedProfile', conn = Connection {connId}} = (ConnectionId connId, XInfo mergedProfile')
ctMsgReq :: ChangedProfileContact -> Either ChatError SndMessage -> Either ChatError ChatMsgReq
ctMsgReq ChangedProfileContact {conn} =
fmap $ \SndMessage {msgId, msgBody} ->
(conn, MsgFlags {notification = hasNotification XInfo_}, (vrValue msgBody, [msgId]))
setMyAddressData :: User -> UserContactLink -> CM UserContactLink
setMyAddressData user ucl@UserContactLink {userContactLinkId, connLinkContact = CCLink connFullLink _sLnk_, addressSettings} = do
conn <- withFastStore $ \db -> getUserAddressConnection db vr user
let shortLinkProfile = userProfileDirect user Nothing Nothing True
-- TODO [short links] do not save address to server if data did not change, spinners, error handling
userData = contactShortLinkData shortLinkProfile $ Just addressSettings
userLinkData = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData}
sLnk <- shortenShortLink' =<< withAgent (\a -> setConnShortLink a nm (aConnId conn) SCMContact userLinkData Nothing)
withFastStore' $ \db -> setUserContactLinkShortLink db userContactLinkId sLnk
let autoAccept' = (\aa -> aa {acceptIncognito = False}) <$> autoAccept addressSettings
ucl' = (ucl :: UserContactLink) {connLinkContact = CCLink connFullLink (Just sLnk), shortLinkDataSet = True, shortLinkLargeDataSet = BoolDef True, addressSettings = addressSettings {autoAccept = autoAccept'}}
pure ucl'
updateContactPrefs :: User -> Contact -> Preferences -> CM ChatResponse
updateContactPrefs _ ct@Contact {activeConn = Nothing} _ = throwChatError $ CEContactNotActive ct
updateContactPrefs user@User {userId} ct@Contact {activeConn = Just Connection {customUserProfileId}, userPreferences = contactUserPrefs} contactUserPrefs'
| contactUserPrefs == contactUserPrefs' = pure $ CRContactPrefsUpdated user ct ct
| otherwise = do
assertDirectAllowed user MDSnd ct XInfo_
ct' <- withStore' $ \db -> updateContactUserPreferences db user ct contactUserPrefs'
incognitoProfile <- forM customUserProfileId $ \profileId -> withStore $ \db -> getProfileById db userId profileId
let mergedProfile = userProfileDirect user (fromLocalProfile <$> incognitoProfile) (Just ct) False
mergedProfile' = userProfileDirect user (fromLocalProfile <$> incognitoProfile) (Just ct') False
when (mergedProfile' /= mergedProfile) $
withContactLock "updateContactPrefs" (contactId' ct) $ do
void (sendDirectContactMessage user ct' $ XInfo mergedProfile') `catchAllErrors` eToView
lift . when (directOrUsed ct') $ createSndFeatureItems user ct ct'
pure $ CRContactPrefsUpdated user ct ct'
runUpdateGroupProfile :: User -> GroupInfo -> GroupProfile -> CM ChatResponse
runUpdateGroupProfile user gInfo@GroupInfo {businessChat, groupProfile = p@GroupProfile {displayName = n}} p'@GroupProfile {displayName = n'} = do
assertUserGroupRole gInfo GROwner
when (n /= n') $ checkValidName n'
gInfo' <- withStore $ \db -> updateGroupProfile db user gInfo p'
msg <- case businessChat of
Just BusinessChatInfo {businessId} -> do
ms <- withStore' $ \db -> getGroupMembers db vr user gInfo'
let (newMs, oldMs) = partition (\m -> maxVersion (memberChatVRange m) >= businessChatPrefsVersion) ms
-- this is a fallback to send the members with the old version correct profile of the business when preferences change
unless (null oldMs) $ do
GroupMember {memberProfile = LocalProfile {displayName, fullName, shortDescr, image}} <-
withStore $ \db -> getGroupMemberByMemberId db vr user gInfo' businessId
let p'' = p' {displayName, fullName, shortDescr, image} :: GroupProfile
recipients = filter memberCurrentOrPending oldMs
void $ sendGroupMessage user gInfo' Nothing recipients (XGrpInfo p'')
let ps' = fromMaybe defaultBusinessGroupPrefs $ groupPreferences p'
recipients = filter memberCurrentOrPending newMs
sendGroupMessage user gInfo' Nothing recipients $ XGrpPrefs ps'
Nothing -> do
setGroupLinkData' nm user gInfo'
recipients <- getRecipients
sendGroupMessage user gInfo' Nothing recipients (XGrpInfo p')
where
getRecipients
| isTrue (useRelays gInfo') = withFastStore' $ \db -> getGroupRelays db vr user gInfo'
| otherwise = do
ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo'
pure $ filter memberCurrentOrPending ms
let cd = CDGroupSnd gInfo' Nothing
unless (sameGroupProfileInfo p p') $ do
ci <- saveSndChatItem user cd msg (CISndGroupEvent $ SGEGroupUpdated p')
toView $ CEvtNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo' Nothing) ci]
createGroupFeatureChangedItems user cd CISndGroupFeature gInfo gInfo'
pure $ CRGroupUpdated user gInfo gInfo' Nothing
checkValidName :: GroupName -> CM ()
checkValidName displayName = do
when (T.null displayName) $ throwChatError CEInvalidDisplayName {displayName, validName = ""}
let validName = T.pack $ mkValidName $ T.unpack displayName
when (displayName /= validName) $ throwChatError CEInvalidDisplayName {displayName, validName}
assertUserGroupRole :: GroupInfo -> GroupMemberRole -> CM ()
assertUserGroupRole g@GroupInfo {membership} requiredRole = do
let GroupMember {memberRole = membershipMemRole} = membership
when (membershipMemRole < requiredRole) $ throwChatError $ CEGroupUserRole g requiredRole
when (memberStatus membership == GSMemInvited) $ throwChatError (CEGroupNotJoined g)
when (memberRemoved membership) $ throwChatError CEGroupMemberUserRemoved
unless (memberActive membership) $ throwChatError CEGroupMemberNotActive
delGroupChatItemsForMembers :: User -> GroupInfo -> Maybe GroupChatScopeInfo -> [GroupMember] -> [CChatItem 'CTGroup] -> CM [ChatItemDeletion]
delGroupChatItemsForMembers user gInfo chatScopeInfo ms items = do
assertDeletable gInfo items
assertUserGroupRole gInfo GRModerator
let msgMemIds = itemsMsgMemIds gInfo items
events = L.nonEmpty $ map (\(msgId, memId) -> XMsgDel msgId (Just memId) $ toMsgScope gInfo <$> chatScopeInfo) msgMemIds
mapM_ (sendGroupMessages_ user gInfo ms) events
delGroupChatItems user gInfo chatScopeInfo items True
where
assertDeletable :: GroupInfo -> [CChatItem 'CTGroup] -> CM ()
assertDeletable GroupInfo {membership = GroupMember {memberRole = membershipMemRole}} items' =
unless (all itemDeletable items') $ throwChatError CEInvalidChatItemDelete
where
itemDeletable :: CChatItem 'CTGroup -> Bool
itemDeletable (CChatItem _ ChatItem {chatDir, meta = CIMeta {itemSharedMsgId}}) =
case chatDir of
CIGroupRcv GroupMember {memberRole} -> membershipMemRole >= memberRole && isJust itemSharedMsgId
CIGroupSnd -> isJust itemSharedMsgId
itemsMsgMemIds :: GroupInfo -> [CChatItem 'CTGroup] -> [(SharedMsgId, MemberId)]
itemsMsgMemIds GroupInfo {membership = GroupMember {memberId = membershipMemId}} = mapMaybe itemMsgMemIds
where
itemMsgMemIds :: CChatItem 'CTGroup -> Maybe (SharedMsgId, MemberId)
itemMsgMemIds (CChatItem _ ChatItem {chatDir, meta = CIMeta {itemSharedMsgId}}) =
join <$> forM itemSharedMsgId $ \msgId -> Just $ case chatDir of
CIGroupRcv GroupMember {memberId} -> (msgId, memberId)
CIGroupSnd -> (msgId, membershipMemId)
delGroupChatItems :: User -> GroupInfo -> Maybe GroupChatScopeInfo -> [CChatItem 'CTGroup] -> Bool -> CM [ChatItemDeletion]
delGroupChatItems user gInfo@GroupInfo {membership} chatScopeInfo items moderation = do
deletedTs <- liftIO getCurrentTime
when moderation $ do
ciIds <- concat <$> withStore' (\db -> forM items $ \(CChatItem _ ci) -> markMessageReportsDeleted db user gInfo ci membership deletedTs)
unless (null ciIds) $ toView $ CEvtGroupChatItemsDeleted user gInfo ciIds True (Just membership)
let m = if moderation then Just membership else Nothing
if groupFeatureUserAllowed SGFFullDelete gInfo
then deleteGroupCIs user gInfo chatScopeInfo items m deletedTs
else markGroupCIsDeleted user gInfo chatScopeInfo items m deletedTs
updateGroupProfileByName :: GroupName -> (GroupProfile -> GroupProfile) -> CM ChatResponse
updateGroupProfileByName gName update = withUser $ \user -> do
gInfo@GroupInfo {groupProfile = p} <- withStore $ \db ->
getGroupIdByName db user gName >>= getGroupInfo db vr user
runUpdateGroupProfile user gInfo $ update p
withCurrentCall :: ContactId -> (User -> Contact -> Call -> CM (Maybe Call)) -> CM ChatResponse
withCurrentCall ctId action = do
(user, ct) <- withStore $ \db -> do
user <- getUserByContactId db ctId
(user,) <$> getContact db vr user ctId
calls <- asks currentCalls
withContactLock "currentCall" ctId $
atomically (TM.lookup ctId calls) >>= \case
Nothing -> throwChatError CENoCurrentCall
Just call@Call {contactId}
| ctId == contactId -> do
call_ <- action user ct call
case call_ of
Just call' -> do
unless (isRcvInvitation call') $ withStore' $ \db -> deleteCalls db user ctId
atomically $ TM.insert ctId call' calls
_ -> do
withStore' $ \db -> deleteCalls db user ctId
atomically $ TM.delete ctId calls
ok user
| otherwise -> throwChatError $ CECallContact contactId
withServerProtocol :: ProtocolTypeI p => SProtocolType p -> (UserProtocol p => CM a) -> CM a
withServerProtocol p action = case userProtocol p of
Just Dict -> action
_ -> throwChatError $ CEServerProtocol $ AProtocolType p
validateAllUsersServers :: UserServersClass u => Int64 -> [u] -> CM [UserServersError]
validateAllUsersServers currUserId userServers = withFastStore $ \db -> do
users' <- filter (\User {userId} -> userId /= currUserId) <$> liftIO (getUsers db)
others <- mapM (getUserOperatorServers db) users'
pure $ validateUserServers userServers others
where
getUserOperatorServers :: DB.Connection -> User -> ExceptT StoreError IO (User, [UserOperatorServers])
getUserOperatorServers db user = do
uss <- liftIO . groupByOperator =<< getUserServers db user
pure (user, map updatedUserSrvs uss)
updatedUserSrvs uss = uss {operator = updatedOp <$> operator' uss} :: UserOperatorServers
updatedOp op = fromMaybe op $ find matchingOp $ mapMaybe operator' userServers
where
matchingOp op' = operatorId op' == operatorId op
forwardFile :: ChatName -> FileTransferId -> (ChatName -> CryptoFile -> ChatCommand) -> CM ChatResponse
forwardFile chatName fileId sendCommand = withUser $ \user -> do
withStore (\db -> getFileTransfer db user fileId) >>= \case
FTRcv RcvFileTransfer {fileStatus = RFSComplete filePath, cryptoArgs} -> forward filePath cryptoArgs
FTSnd {fileTransferMeta = FileTransferMeta {filePath, xftpSndFile}} -> forward filePath $ xftpSndFile >>= \XFTPSndFile {cryptoArgs} -> cryptoArgs
_ -> throwChatError CEFileNotReceived {fileId}
where
forward path cfArgs = processChatCommand vr nm $ sendCommand chatName $ CryptoFile path cfArgs
getGroupAndMemberId :: User -> GroupName -> ContactName -> CM (GroupId, GroupMemberId)
getGroupAndMemberId user gName groupMemberName =
withStore $ \db -> do
groupId <- getGroupIdByName db user gName
groupMemberId <- getGroupMemberIdByName db user groupId groupMemberName
pure (groupId, groupMemberId)
sendGrpInvitation :: User -> Contact -> GroupInfo -> GroupMember -> ConnReqInvitation -> CM ()
sendGrpInvitation user ct@Contact {contactId, localDisplayName} gInfo@GroupInfo {groupId, groupProfile, membership, businessChat} GroupMember {groupMemberId, memberId, memberRole = memRole} cReq = do
currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo
let GroupMember {memberRole = userRole, memberId = userMemberId} = membership
groupInv =
GroupInvitation
{ fromMember = MemberIdRole userMemberId userRole,
invitedMember = MemberIdRole memberId memRole,
connRequest = cReq,
groupProfile,
business = businessChat,
groupLinkId = Nothing,
groupSize = Just currentMemCount
}
(msg, _) <- sendDirectContactMessage user ct $ XGrpInv groupInv
let content = CISndGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole
timed_ <- contactCITimed ct
ci <- saveSndChatItem' user (CDDirectSnd ct) msg content Nothing Nothing Nothing timed_ False
toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci]
forM_ (timed_ >>= timedDeleteAt') $
startProximateTimedItemThread user (ChatRef CTDirect contactId Nothing, chatItemId' ci)
drgRandomBytes :: Int -> CM ByteString
drgRandomBytes n = asks random >>= atomically . C.randomBytes n
privateGetUser :: UserId -> CM User
privateGetUser userId =
tryAllErrors (withStore (`getUser` userId)) >>= \case
Left _ -> throwChatError CEUserUnknown
Right user -> pure user
validateUserPassword :: User -> User -> Maybe UserPwd -> CM ()
validateUserPassword = validateUserPassword_ . Just
validateUserPassword_ :: Maybe User -> User -> Maybe UserPwd -> CM ()
validateUserPassword_ user_ User {userId = userId', viewPwdHash} viewPwd_ =
forM_ viewPwdHash $ \pwdHash ->
let userId_ = (\User {userId} -> userId) <$> user_
pwdOk = case viewPwd_ of
Nothing -> userId_ == Just userId'
Just (UserPwd viewPwd) -> validPassword viewPwd pwdHash
in unless pwdOk $ throwChatError CEUserUnknown
validPassword :: Text -> UserPwdHash -> Bool
validPassword pwd UserPwdHash {hash = B64UrlByteString hash, salt = B64UrlByteString salt} =
hash == C.sha512Hash (encodeUtf8 pwd <> salt)
setUserNotifications :: UserId -> Bool -> CM ChatResponse
setUserNotifications userId' showNtfs = withUser $ \user -> do
user' <- privateGetUser userId'
case viewPwdHash user' of
Just _ -> throwChatError $ CEHiddenUserAlwaysMuted userId'
_ -> setUserPrivacy user user' {showNtfs}
setUserPrivacy :: User -> User -> CM ChatResponse
setUserPrivacy user@User {userId} user'@User {userId = userId'}
| userId == userId' = do
asks currentUser >>= atomically . (`writeTVar` Just user')
withFastStore' (`updateUserPrivacy` user')
pure $ CRUserPrivacy {user = user', updatedUser = user'}
| otherwise = do
withFastStore' (`updateUserPrivacy` user')
pure $ CRUserPrivacy {user, updatedUser = user'}
checkDeleteChatUser :: User -> CM ()
checkDeleteChatUser user@User {userId} = do
users <- withFastStore' getUsers
let otherVisible = filter (\User {userId = userId', viewPwdHash} -> userId /= userId' && isNothing viewPwdHash) users
when (activeUser user && length otherVisible > 0) $ throwChatError (CECantDeleteActiveUser userId)
deleteChatUser :: User -> Bool -> CM ChatResponse
deleteChatUser user delSMPQueues = do
filesInfo <- withFastStore' (`getUserFileInfo` user)
deleteCIFiles user filesInfo
withAgent (\a -> deleteUser a (aUserId user) delSMPQueues)
`catchAllErrors` \case
e@(ChatErrorAgent NO_USER _ _) -> eToView e
e -> throwError e
withFastStore' (`deleteUserRecord` user)
when (activeUser user) $ chatWriteVar currentUser Nothing
ok_
updateChatSettings :: ChatName -> (ChatSettings -> ChatSettings) -> CM ChatResponse
updateChatSettings (ChatName cType name) updateSettings = withUser $ \user -> do
(chatId, chatSettings) <- case cType of
CTDirect -> withFastStore $ \db -> do
ctId <- getContactIdByName db user name
Contact {chatSettings} <- getContact db vr user ctId
pure (ctId, chatSettings)
CTGroup ->
withFastStore $ \db -> do
gId <- getGroupIdByName db user name
GroupInfo {chatSettings} <- getGroupInfo db vr user gId
pure (gId, chatSettings)
_ -> throwCmdError "not supported"
processChatCommand vr nm $ APISetChatSettings (ChatRef cType chatId Nothing) $ updateSettings chatSettings
connectPlan :: User -> AConnectionLink -> CM (ACreatedConnLink, ConnectionPlan)
connectPlan user (ACL SCMInvitation cLink) = case cLink of
CLFull cReq -> invitationReqAndPlan cReq Nothing Nothing
CLShort l -> do
let l' = serverShortLink l
knownLinkPlans l' >>= \case
Just r -> pure r
Nothing -> do
(cReq, cData) <- getShortLinkConnReq user l'
contactSLinkData_ <- liftIO $ decodeShortLinkData cData
invitationReqAndPlan cReq (Just l') contactSLinkData_
where
knownLinkPlans l' = withFastStore $ \db -> do
let inv cReq = ACCL SCMInvitation $ CCLink cReq (Just l')
liftIO (getConnectionEntityViaShortLink db vr user l') >>= \case
Just (cReq, ent) -> pure $ Just (inv cReq, invitationEntityPlan Nothing ent)
-- deleted contact is returned as known, as invitation link cannot be re-used too connect anyway
Nothing -> bimap inv (CPInvitationLink . ILPKnown) <$$> getContactViaShortLinkToConnect db vr user l'
invitationReqAndPlan cReq sLnk_ contactSLinkData_ = do
plan <- invitationRequestPlan user cReq contactSLinkData_ `catchAllErrors` (pure . CPError)
pure (ACCL SCMInvitation (CCLink cReq sLnk_), plan)
connectPlan user (ACL SCMContact cLink) = case cLink of
CLFull cReq -> do
plan <- contactOrGroupRequestPlan user cReq `catchAllErrors` (pure . CPError)
pure (ACCL SCMContact $ CCLink cReq Nothing, plan)
CLShort l@(CSLContact _ ct _ _) -> do
let l' = serverShortLink l
con cReq = ACCL SCMContact $ CCLink cReq (Just l')
gPlan (cReq, g) = if memberRemoved (membership g) then Nothing else Just (con cReq, CPGroupLink (GLPKnown g))
case ct of
CCTContact ->
knownLinkPlans >>= \case
Just r -> pure r
Nothing -> do
(cReq, cData) <- getShortLinkConnReq user l'
withFastStore' (\db -> getContactWithoutConnViaShortAddress db vr user l') >>= \case
Just ct' | not (contactDeleted ct') -> pure (con cReq, CPContactAddress (CAPContactViaAddress ct'))
_ -> do
contactSLinkData_ <- liftIO $ decodeShortLinkData cData
plan <- contactRequestPlan user cReq contactSLinkData_
pure (con cReq, plan)
where
knownLinkPlans = withFastStore $ \db ->
liftIO (getUserContactLinkViaShortLink db user l') >>= \case
Just UserContactLink {connLinkContact = CCLink cReq _} -> pure $ Just (con cReq, CPContactAddress CAPOwnLink)
Nothing ->
getContactViaShortLinkToConnect db vr user l' >>= \case
Just (cReq, ct') -> pure $ if contactDeleted ct' then Nothing else Just (con cReq, CPContactAddress (CAPKnown ct'))
Nothing -> (gPlan =<<) <$> getGroupViaShortLinkToConnect db vr user l'
CCTGroup ->
knownLinkPlans >>= \case
Just r -> pure r
Nothing -> do
(cReq, cData) <- getShortLinkConnReq user l'
groupSLinkData_ <- liftIO $ decodeShortLinkData cData
plan <- groupJoinRequestPlan user cReq groupSLinkData_
pure (con cReq, plan)
where
knownLinkPlans = withFastStore $ \db ->
liftIO (getGroupInfoViaUserShortLink db vr user l') >>= \case
Just (cReq, g) -> pure $ Just (con cReq, CPGroupLink (GLPOwnLink g))
Nothing -> (gPlan =<<) <$> getGroupViaShortLinkToConnect db vr user l'
CCTChannel -> throwCmdError "channel links are not supported in this version"
CCTRelay -> throwCmdError "chat relay links are not supported in this version"
connectWithPlan :: User -> IncognitoEnabled -> ACreatedConnLink -> ConnectionPlan -> CM ChatResponse
connectWithPlan user@User {userId} incognito ccLink plan
| connectionPlanProceed plan = do
case plan of CPError e -> eToView e; _ -> pure ()
case plan of
CPContactAddress (CAPContactViaAddress Contact {contactId}) ->
processChatCommand vr nm $ APIConnectContactViaAddress userId incognito contactId
_ -> processChatCommand vr nm $ APIConnect userId incognito $ Just ccLink
| otherwise = pure $ CRConnectionPlan user ccLink plan
invitationRequestPlan :: User -> ConnReqInvitation -> Maybe ContactShortLinkData -> CM ConnectionPlan
invitationRequestPlan user cReq contactSLinkData_ = do
maybe (CPInvitationLink (ILPOk contactSLinkData_)) (invitationEntityPlan contactSLinkData_)
<$> withFastStore' (\db -> getConnectionEntityByConnReq db vr user $ invCReqSchemas cReq)
where
invCReqSchemas :: ConnReqInvitation -> (ConnReqInvitation, ConnReqInvitation)
invCReqSchemas (CRInvitationUri crData e2e) =
( CRInvitationUri crData {crScheme = SSSimplex} e2e,
CRInvitationUri crData {crScheme = simplexChat} e2e
)
invitationEntityPlan :: Maybe ContactShortLinkData -> ConnectionEntity -> ConnectionPlan
invitationEntityPlan contactSLinkData_ = \case
RcvDirectMsgConnection Connection {connStatus, contactConnInitiated} ct_ -> case ct_ of
Just ct
| contactActive ct -> CPInvitationLink (ILPKnown ct)
| otherwise -> CPInvitationLink (ILPOk contactSLinkData_)
Nothing
| connStatus == ConnNew && contactConnInitiated -> CPInvitationLink ILPOwnLink
| connStatus == ConnPrepared -> CPInvitationLink (ILPOk contactSLinkData_)
| otherwise -> CPInvitationLink (ILPConnecting Nothing)
_ -> CPError $ ChatError $ CECommandError "found connection entity is not RcvDirectMsgConnection"
contactOrGroupRequestPlan :: User -> ConnReqContact -> CM ConnectionPlan
contactOrGroupRequestPlan user cReq@(CRContactUri crData) = do
let ConnReqUriData {crClientData} = crData
groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli
case groupLinkId of
Nothing -> contactRequestPlan user cReq Nothing
Just _ -> groupJoinRequestPlan user cReq Nothing
contactRequestPlan :: User -> ConnReqContact -> Maybe ContactShortLinkData -> CM ConnectionPlan
contactRequestPlan user (CRContactUri crData) contactSLinkData_ = do
let cReqSchemas = contactCReqSchemas crData
cReqHashes = bimap contactCReqHash contactCReqHash cReqSchemas
withFastStore' (\db -> getUserContactLinkByConnReq db user cReqSchemas) >>= \case
Just _ -> pure $ CPContactAddress CAPOwnLink
Nothing ->
withFastStore' (\db -> getContactConnEntityByConnReqHash db vr user cReqHashes) >>= \case
Nothing ->
withFastStore' (\db -> getContactWithoutConnViaAddress db vr user cReqSchemas) >>= \case
Just ct | not (contactDeleted ct) -> pure $ CPContactAddress (CAPContactViaAddress ct)
_ -> pure $ CPContactAddress (CAPOk contactSLinkData_)
Just (RcvDirectMsgConnection Connection {connStatus} Nothing)
| connStatus == ConnPrepared -> pure $ CPContactAddress (CAPOk contactSLinkData_)
| otherwise -> pure $ CPContactAddress CAPConnectingConfirmReconnect
Just (RcvDirectMsgConnection _ (Just ct))
| not (contactReady ct) && contactActive ct -> pure $ CPContactAddress (CAPConnectingProhibit ct)
| contactDeleted ct -> pure $ CPContactAddress (CAPOk contactSLinkData_)
| otherwise -> pure $ CPContactAddress (CAPKnown ct)
-- TODO [short links] RcvGroupMsgConnection branch is deprecated? (old group link protocol?)
Just (RcvGroupMsgConnection _ gInfo _) -> groupPlan gInfo Nothing
Just _ -> throwCmdError "found connection entity is not RcvDirectMsgConnection or RcvGroupMsgConnection"
groupJoinRequestPlan :: User -> ConnReqContact -> Maybe GroupShortLinkData -> CM ConnectionPlan
groupJoinRequestPlan user (CRContactUri crData) groupSLinkData_ = do
let cReqSchemas = contactCReqSchemas crData
cReqHashes = bimap contactCReqHash contactCReqHash cReqSchemas
withFastStore' (\db -> getGroupInfoByUserContactLinkConnReq db vr user cReqSchemas) >>= \case
Just g -> pure $ CPGroupLink (GLPOwnLink g)
Nothing -> do
connEnt_ <- withFastStore' $ \db -> getContactConnEntityByConnReqHash db vr user cReqHashes
gInfo_ <- withFastStore' $ \db -> getGroupInfoByGroupLinkHash db vr user cReqHashes
case (gInfo_, connEnt_) of
(Nothing, Nothing) -> pure $ CPGroupLink (GLPOk groupSLinkData_)
-- TODO [short links] RcvDirectMsgConnection branches are deprecated? (old group link protocol?)
(Nothing, Just (RcvDirectMsgConnection _conn Nothing)) -> pure $ CPGroupLink GLPConnectingConfirmReconnect
(Nothing, Just (RcvDirectMsgConnection _ (Just ct)))
| not (contactReady ct) && contactActive ct -> pure $ CPGroupLink (GLPConnectingProhibit gInfo_)
| otherwise -> pure $ CPGroupLink (GLPOk groupSLinkData_)
(Nothing, Just _) -> throwCmdError "found connection entity is not RcvDirectMsgConnection"
(Just gInfo, _) -> groupPlan gInfo groupSLinkData_
groupPlan :: GroupInfo -> Maybe GroupShortLinkData -> CM ConnectionPlan
groupPlan gInfo@GroupInfo {membership} groupSLinkData_
| memberStatus membership == GSMemRejected = pure $ CPGroupLink (GLPKnown gInfo)
| not (memberActive membership) && not (memberRemoved membership) =
pure $ CPGroupLink (GLPConnectingProhibit $ Just gInfo)
| memberActive membership = pure $ CPGroupLink (GLPKnown gInfo)
| otherwise = pure $ CPGroupLink (GLPOk groupSLinkData_)
contactCReqSchemas :: ConnReqUriData -> (ConnReqContact, ConnReqContact)
contactCReqSchemas crData =
( CRContactUri crData {crScheme = SSSimplex},
CRContactUri crData {crScheme = simplexChat}
)
contactCReqHash :: ConnReqContact -> ConnReqUriHash
contactCReqHash = ConnReqUriHash . C.sha256Hash . strEncode
getShortLinkConnReq :: User -> ConnShortLink m -> CM (ConnectionRequestUri m, ConnLinkData m)
getShortLinkConnReq user l = do
l' <- restoreShortLink' l
(cReq, cData) <- withAgent $ \a -> getConnShortLink a nm (aUserId user) l'
case cData of
ContactLinkData _ UserContactData {direct} | not direct -> throwChatError CEUnsupportedConnReq
_ -> pure ()
pure (cReq, cData)
-- This function is needed, as UI uses simplex:/ schema in message view, so that the links can be handled without browser,
-- and short links are stored with server hostname schema, so they wouldn't match without it.
serverShortLink :: ConnShortLink m -> ConnShortLink m
serverShortLink = \case
CSLInvitation _ srv lnkId linkKey -> CSLInvitation SLSServer srv lnkId linkKey
CSLContact _ ct srv linkKey -> CSLContact SLSServer ct srv linkKey
restoreShortLink' l = (`restoreShortLink` l) <$> asks (shortLinkPresetServers . config)
contactShortLinkData :: Profile -> Maybe AddressSettings -> UserLinkData
contactShortLinkData p settings =
let msg = autoReply =<< settings
business = maybe False businessAddress settings
contactData = ContactShortLinkData p msg business
in encodeShortLinkData contactData
updatePCCShortLinkData :: PendingContactConnection -> Profile -> CM (Maybe ShortLinkInvitation)
updatePCCShortLinkData conn@PendingContactConnection {connLinkInv} profile =
forM (connShortLink =<< connLinkInv) $ \_ -> do
let userData = contactShortLinkData profile Nothing
userLinkData = UserInvLinkData userData
shortenShortLink' =<< withAgent (\a -> setConnShortLink a nm (aConnId' conn) SCMInvitation userLinkData Nothing)
updateCIGroupInvitationStatus :: User -> GroupInfo -> CIGroupInvitationStatus -> CM ()
updateCIGroupInvitationStatus user GroupInfo {groupId} newStatus = do
AChatItem _ _ cInfo ChatItem {content, meta = CIMeta {itemId}} <- withFastStore $ \db -> getChatItemByGroupId db vr user groupId
case (cInfo, content) of
(DirectChat ct@Contact {contactId}, CIRcvGroupInvitation ciGroupInv@CIGroupInvitation {status} memRole)
| status == CIGISPending -> do
let aciContent = ACIContent SMDRcv $ CIRcvGroupInvitation (ciGroupInv {status = newStatus} :: CIGroupInvitation) memRole
timed_ <- contactCITimed ct
updateDirectChatItemView user ct itemId aciContent False False timed_ Nothing
forM_ (timed_ >>= timedDeleteAt') $
startProximateTimedItemThread user (ChatRef CTDirect contactId Nothing, itemId)
_ -> pure () -- prohibited
assertAllowedContent :: MsgContent -> CM ()
assertAllowedContent = \case
MCReport {} -> throwCmdError "sending reports via this API is not supported"
_ -> pure ()
assertAllowedContent' :: ComposedMessage -> CM ()
assertAllowedContent' ComposedMessage {msgContent} = assertAllowedContent msgContent
assertNoMentions :: ComposedMessage -> CM ()
assertNoMentions ComposedMessage {mentions}
| null mentions = pure ()
| otherwise = throwCmdError "mentions are not supported in this chat"
sendContactContentMessages :: User -> ContactId -> Bool -> Maybe Int -> NonEmpty ComposedMessageReq -> CM ChatResponse
sendContactContentMessages user contactId live itemTTL cmrs = do
assertMultiSendable live cmrs
ct <- withFastStore $ \db -> getContact db vr user contactId
assertDirectAllowed user MDSnd ct XMsgNew_
assertVoiceAllowed ct
processComposedMessages ct
where
assertVoiceAllowed :: Contact -> CM ()
assertVoiceAllowed ct =
when (not (featureAllowed SCFVoice forUser ct) && any (\(ComposedMessage {msgContent}, _, _, _) -> isVoice msgContent) cmrs) $
throwCmdError $ "feature not allowed " <> T.unpack (chatFeatureNameText CFVoice)
processComposedMessages :: Contact -> CM ChatResponse
processComposedMessages ct = do
(fInvs_, ciFiles_) <- L.unzip <$> setupSndFileTransfers
timed_ <- sndContactCITimed live ct itemTTL
(msgContainers, quotedItems_) <- L.unzip <$> prepareMsgs (L.zip cmrs fInvs_) timed_
msgs_ <- sendDirectContactMessages user ct $ L.map XMsgNew msgContainers
let itemsData = prepareSndItemsData (L.toList cmrs) (L.toList ciFiles_) (L.toList quotedItems_) msgs_
when (length itemsData /= length cmrs) $ logError "sendContactContentMessages: cmrs and itemsData length mismatch"
r@(_, cis) <- partitionEithers <$> saveSndChatItems user (CDDirectSnd ct) itemsData timed_ live
processSendErrs r
forM_ (timed_ >>= timedDeleteAt') $ \deleteAt ->
forM_ cis $ \ci ->
startProximateTimedItemThread user (ChatRef CTDirect contactId Nothing, chatItemId' ci) deleteAt
pure $ CRNewChatItems user (map (AChatItem SCTDirect SMDSnd (DirectChat ct)) cis)
where
setupSndFileTransfers :: CM (NonEmpty (Maybe FileInvitation, Maybe (CIFile 'MDSnd)))
setupSndFileTransfers =
forM cmrs $ \(ComposedMessage {fileSource = file_}, _, _, _) -> case file_ of
Just file -> do
fileSize <- checkSndFile file
(fInv, ciFile) <- xftpSndFileTransfer user file fileSize 1 $ CGContact ct
pure (Just fInv, Just ciFile)
Nothing -> pure (Nothing, Nothing)
prepareMsgs :: NonEmpty (ComposedMessageReq, Maybe FileInvitation) -> Maybe CITimed -> CM (NonEmpty (MsgContainer, Maybe (CIQuote 'CTDirect)))
prepareMsgs cmsFileInvs timed_ = withFastStore $ \db ->
forM cmsFileInvs $ \((ComposedMessage {quotedItemId, msgContent = mc}, itemForwarded, _, _), fInv_) -> do
case (quotedItemId, itemForwarded) of
(Nothing, Nothing) -> pure (MCSimple (ExtMsgContent mc M.empty fInv_ (ttl' <$> timed_) (justTrue live) Nothing), Nothing)
(Nothing, Just _) -> pure (MCForward (ExtMsgContent mc M.empty fInv_ (ttl' <$> timed_) (justTrue live) Nothing), Nothing)
(Just qiId, Nothing) -> do
CChatItem _ qci@ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, formattedText, file} <-
getDirectChatItem db user contactId qiId
(origQmc, qd, sent) <- quoteData qci
let msgRef = MsgRef {msgId = itemSharedMsgId, sentAt = itemTs, sent, memberId = Nothing}
qmc = quoteContent mc origQmc file
quotedItem = CIQuote {chatDir = qd, itemId = Just qiId, sharedMsgId = itemSharedMsgId, sentAt = itemTs, content = qmc, formattedText}
pure (MCQuote QuotedMsg {msgRef, content = qmc} (ExtMsgContent mc M.empty fInv_ (ttl' <$> timed_) (justTrue live) Nothing), Just quotedItem)
(Just _, Just _) -> throwError SEInvalidQuote
where
quoteData :: ChatItem c d -> ExceptT StoreError IO (MsgContent, CIQDirection 'CTDirect, Bool)
quoteData ChatItem {meta = CIMeta {itemDeleted = Just _}} = throwError SEInvalidQuote
quoteData ChatItem {content = CISndMsgContent qmc} = pure (qmc, CIQDirectSnd, True)
quoteData ChatItem {content = CIRcvMsgContent qmc} = pure (qmc, CIQDirectRcv, False)
quoteData _ = throwError SEInvalidQuote
sendGroupContentMessages :: User -> GroupInfo -> Maybe GroupChatScope -> Bool -> Maybe Int -> NonEmpty ComposedMessageReq -> CM ChatResponse
sendGroupContentMessages user gInfo scope live itemTTL cmrs = do
assertMultiSendable live cmrs
chatScopeInfo <- mapM (getChatScopeInfo vr user) scope
recipients <- getGroupRecipients vr user gInfo chatScopeInfo modsCompatVersion
sendGroupContentMessages_ user gInfo scope chatScopeInfo recipients live itemTTL cmrs
where
hasReport = any (\(ComposedMessage {msgContent}, _, _, _) -> isReport msgContent) cmrs
modsCompatVersion = if hasReport then contentReportsVersion else groupKnockingVersion
sendGroupContentMessages_ :: User -> GroupInfo -> Maybe GroupChatScope -> Maybe GroupChatScopeInfo -> [GroupMember] -> Bool -> Maybe Int -> NonEmpty ComposedMessageReq -> CM ChatResponse
sendGroupContentMessages_ user gInfo@GroupInfo {groupId, membership} scope chatScopeInfo recipients live itemTTL cmrs = do
forM_ allowedRole $ assertUserGroupRole gInfo
assertGroupContentAllowed
processComposedMessages
where
allowedRole :: Maybe GroupMemberRole
allowedRole = case scope of
Nothing -> Just GRAuthor
Just (GCSMemberSupport Nothing)
| memberPending membership -> Nothing
| otherwise -> Just GRObserver
Just (GCSMemberSupport (Just _gmId)) -> Just GRModerator
assertGroupContentAllowed :: CM ()
assertGroupContentAllowed =
case findProhibited (L.toList cmrs) of
Just f -> throwCmdError $ "feature not allowed " <> T.unpack (groupFeatureNameText f)
Nothing -> pure ()
where
findProhibited :: [ComposedMessageReq] -> Maybe GroupFeature
findProhibited =
foldr'
(\(ComposedMessage {fileSource, msgContent = mc}, _, (_, ft), _) acc -> prohibitedGroupContent gInfo membership chatScopeInfo mc ft fileSource True <|> acc)
Nothing
processComposedMessages :: CM ChatResponse
processComposedMessages = do
-- TODO [channels fwd] single description for all recipients
(fInvs_, ciFiles_) <- L.unzip <$> setupSndFileTransfers (length recipients)
timed_ <- sndGroupCITimed live gInfo itemTTL
(chatMsgEvents, quotedItems_) <- L.unzip <$> prepareMsgs (L.zip cmrs fInvs_) timed_
(msgs_, gsr) <- sendGroupMessages user gInfo Nothing recipients chatMsgEvents
let itemsData = prepareSndItemsData (L.toList cmrs) (L.toList ciFiles_) (L.toList quotedItems_) (L.toList msgs_)
cis_ <- saveSndChatItems user (CDGroupSnd gInfo chatScopeInfo) itemsData timed_ live
when (length cis_ /= length cmrs) $ logError "sendGroupContentMessages: cmrs and cis_ length mismatch"
createMemberSndStatuses cis_ msgs_ gsr
let r@(_, cis) = partitionEithers cis_
processSendErrs r
forM_ (timed_ >>= timedDeleteAt') $ \deleteAt ->
forM_ cis $ \ci ->
startProximateTimedItemThread user (ChatRef CTGroup groupId scope, chatItemId' ci) deleteAt
pure $ CRNewChatItems user (map (AChatItem SCTGroup SMDSnd (GroupChat gInfo chatScopeInfo)) cis)
where
setupSndFileTransfers :: Int -> CM (NonEmpty (Maybe FileInvitation, Maybe (CIFile 'MDSnd)))
setupSndFileTransfers n =
forM cmrs $ \(ComposedMessage {fileSource = file_}, _, _, _) -> case file_ of
Just file -> do
fileSize <- checkSndFile file
(fInv, ciFile) <- xftpSndFileTransfer user file fileSize n $ CGGroup gInfo recipients
pure (Just fInv, Just ciFile)
Nothing -> pure (Nothing, Nothing)
prepareMsgs :: NonEmpty (ComposedMessageReq, Maybe FileInvitation) -> Maybe CITimed -> CM (NonEmpty (ChatMsgEvent 'Json, Maybe (CIQuote 'CTGroup)))
prepareMsgs cmsFileInvs timed_ = withFastStore $ \db ->
forM cmsFileInvs $ \((ComposedMessage {quotedItemId, msgContent = mc}, itemForwarded, _, ciMentions), fInv_) ->
let msgScope = toMsgScope gInfo <$> chatScopeInfo
mentions = M.map (\CIMention {memberId} -> MsgMention {memberId}) ciMentions
in prepareGroupMsg db user gInfo msgScope mc mentions quotedItemId itemForwarded fInv_ timed_ live
createMemberSndStatuses ::
[Either ChatError (ChatItem 'CTGroup 'MDSnd)] ->
NonEmpty (Either ChatError SndMessage) ->
GroupSndResult ->
CM ()
createMemberSndStatuses cis_ msgs_ GroupSndResult {sentTo, pending, forwarded} = do
let msgToItem = mapMsgToItem
withFastStore' $ \db -> do
forM_ sentTo (processSentTo db msgToItem)
forM_ forwarded (processForwarded db)
forM_ pending (processPending db msgToItem)
where
mapMsgToItem :: Map MessageId ChatItemId
mapMsgToItem = foldr' addItem M.empty (zip (L.toList msgs_) cis_)
where
addItem (Right SndMessage {msgId}, Right ci) m = M.insert msgId (chatItemId' ci) m
addItem _ m = m
processSentTo :: DB.Connection -> Map MessageId ChatItemId -> (GroupMemberId, Either ChatError [MessageId], Either ChatError ([Int64], PQEncryption)) -> IO ()
processSentTo db msgToItem (mId, msgIds_, deliveryResult) = forM_ msgIds_ $ \msgIds -> do
let ciIds = mapMaybe (`M.lookup` msgToItem) msgIds
status = case deliveryResult of
Right _ -> GSSNew
Left e -> GSSError $ SndErrOther $ tshow e
forM_ ciIds $ \ciId -> createGroupSndStatus db ciId mId status
processForwarded :: DB.Connection -> GroupMember -> IO ()
processForwarded db GroupMember {groupMemberId} =
forM_ cis_ $ \ci_ ->
forM_ ci_ $ \ci -> createGroupSndStatus db (chatItemId' ci) groupMemberId GSSForwarded
processPending :: DB.Connection -> Map MessageId ChatItemId -> (GroupMemberId, Either ChatError MessageId, Either ChatError ()) -> IO ()
processPending db msgToItem (mId, msgId_, pendingResult) = forM_ msgId_ $ \msgId -> do
let ciId_ = M.lookup msgId msgToItem
status = case pendingResult of
Right _ -> GSSInactive
Left e -> GSSError $ SndErrOther $ tshow e
forM_ ciId_ $ \ciId -> createGroupSndStatus db ciId mId status
assertMultiSendable :: Bool -> NonEmpty ComposedMessageReq -> CM ()
assertMultiSendable live cmrs
| length cmrs == 1 = pure ()
| otherwise =
-- When sending multiple messages only single quote is allowed.
-- This is to support case of sending multiple attachments while also quoting another message.
-- UI doesn't allow composing with multiple quotes, so api prohibits it as well, and doesn't bother
-- batching retrieval of quoted messages (prepareMsgs).
when (live || length (L.filter (\(ComposedMessage {quotedItemId}, _, _, _) -> isJust quotedItemId) cmrs) > 1) $
throwCmdError "invalid multi send: live and more than one quote not supported"
xftpSndFileTransfer :: User -> CryptoFile -> Integer -> Int -> ContactOrGroup -> CM (FileInvitation, CIFile 'MDSnd)
xftpSndFileTransfer user file fileSize n contactOrGroup = do
(fInv, ciFile, ft) <- xftpSndFileTransfer_ user file fileSize n $ Just contactOrGroup
case contactOrGroup of
CGContact Contact {activeConn} -> forM_ activeConn $ \conn ->
withFastStore' $ \db -> createSndFTDescrXFTP db user Nothing conn ft dummyFileDescr
CGGroup _ ms -> forM_ ms $ \m -> saveMemberFD m `catchAllErrors` eToView
where
-- we are not sending files to pending members, same as with inline files
saveMemberFD m@GroupMember {activeConn = Just conn@Connection {connStatus}} =
when ((connStatus == ConnReady || connStatus == ConnSndReady) && not (connDisabled conn)) $
withFastStore' $
\db -> createSndFTDescrXFTP db user (Just m) conn ft dummyFileDescr
saveMemberFD _ = pure ()
pure (fInv, ciFile)
prepareSndItemsData ::
[ComposedMessageReq] ->
[Maybe (CIFile 'MDSnd)] ->
[Maybe (CIQuote c)] ->
[Either ChatError SndMessage] ->
[Either ChatError (NewSndChatItemData c)]
prepareSndItemsData =
zipWith4 $ \(ComposedMessage {msgContent}, itemForwarded, ts, mm) f q -> \case
Right msg -> Right $ NewSndChatItemData msg (CISndMsgContent msgContent) ts mm f q itemForwarded
Left e -> Left e -- step over original error
processSendErrs :: ([ChatError], [ChatItem c d]) -> CM ()
processSendErrs = \case
-- no errors
([], _) -> pure ()
-- at least one item is successfully created
(errs, _ci : _) -> toView $ CEvtChatErrors errs
-- single error
([err], []) -> throwError err
-- multiple errors
(errs@(err : _), []) -> do
toView $ CEvtChatErrors errs
throwError err
getCommandDirectChatItems :: User -> Int64 -> NonEmpty ChatItemId -> CM (Contact, [CChatItem 'CTDirect])
getCommandDirectChatItems user ctId itemIds = do
ct <- withFastStore $ \db -> getContact db vr user ctId
(errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getDirectCI db) (L.toList itemIds))
unless (null errs) $ toView $ CEvtChatErrors errs
pure (ct, items)
where
getDirectCI :: DB.Connection -> ChatItemId -> IO (Either ChatError (CChatItem 'CTDirect))
getDirectCI db itemId = runExceptT . withExceptT ChatErrorStore $ getDirectChatItem db user ctId itemId
getCommandGroupChatItems :: User -> Int64 -> NonEmpty ChatItemId -> CM (GroupInfo, [CChatItem 'CTGroup])
getCommandGroupChatItems user gId itemIds = do
gInfo <- withFastStore $ \db -> getGroupInfo db vr user gId
(errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getGroupCI db gInfo) (L.toList itemIds))
unless (null errs) $ toView $ CEvtChatErrors errs
pure (gInfo, items)
where
getGroupCI :: DB.Connection -> GroupInfo -> ChatItemId -> IO (Either ChatError (CChatItem 'CTGroup))
getGroupCI db gInfo itemId = runExceptT . withExceptT ChatErrorStore $ getGroupCIWithReactions db user gInfo itemId
getCommandLocalChatItems :: User -> Int64 -> NonEmpty ChatItemId -> CM (NoteFolder, [CChatItem 'CTLocal])
getCommandLocalChatItems user nfId itemIds = do
nf <- withStore $ \db -> getNoteFolder db user nfId
(errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getLocalCI db) (L.toList itemIds))
unless (null errs) $ toView $ CEvtChatErrors errs
pure (nf, items)
where
getLocalCI :: DB.Connection -> ChatItemId -> IO (Either ChatError (CChatItem 'CTLocal))
getLocalCI db itemId = runExceptT . withExceptT ChatErrorStore $ getLocalChatItem db user nfId itemId
forwardMsgContent :: ChatItem c d -> CM (Maybe MsgContent)
forwardMsgContent ChatItem {meta = CIMeta {itemDeleted = Just _}} = pure Nothing -- this can be deleted after selection
forwardMsgContent ChatItem {content = CISndMsgContent fmc} = pure $ Just fmc
forwardMsgContent ChatItem {content = CIRcvMsgContent fmc} = pure $ Just fmc
forwardMsgContent _ = throwChatError CEInvalidForward
createNoteFolderContentItems :: User -> NoteFolderId -> NonEmpty ComposedMessageReq -> CM ChatResponse
createNoteFolderContentItems user folderId cmrs = do
assertNoQuotes
nf <- withFastStore $ \db -> getNoteFolder db user folderId
createdAt <- liftIO getCurrentTime
ciFiles_ <- createLocalFiles nf createdAt
let itemsData = prepareLocalItemsData cmrs ciFiles_
cis <- createLocalChatItems user (CDLocalSnd nf) itemsData createdAt
pure $ CRNewChatItems user (map (AChatItem SCTLocal SMDSnd (LocalChat nf)) cis)
where
assertNoQuotes :: CM ()
assertNoQuotes =
when (any (\(ComposedMessage {quotedItemId}, _, _, _) -> isJust quotedItemId) cmrs) $
throwCmdError "createNoteFolderContentItems: quotes not supported"
createLocalFiles :: NoteFolder -> UTCTime -> CM (NonEmpty (Maybe (CIFile 'MDSnd)))
createLocalFiles nf createdAt =
forM cmrs $ \(ComposedMessage {fileSource = file_}, _, _, _) ->
forM file_ $ \cf@CryptoFile {filePath, cryptoArgs} -> do
fsFilePath <- lift $ toFSFilePath filePath
fileSize <- liftIO $ CF.getFileContentsSize $ CryptoFile fsFilePath cryptoArgs
chunkSize <- asks $ fileChunkSize . config
withFastStore' $ \db -> do
fileId <- createLocalFile CIFSSndStored db user nf createdAt cf fileSize chunkSize
pure CIFile {fileId, fileName = takeFileName filePath, fileSize, fileSource = Just cf, fileStatus = CIFSSndStored, fileProtocol = FPLocal}
prepareLocalItemsData ::
NonEmpty ComposedMessageReq ->
NonEmpty (Maybe (CIFile 'MDSnd)) ->
NonEmpty (CIContent 'MDSnd, Maybe (CIFile 'MDSnd), Maybe CIForwardedFrom, (Text, Maybe MarkdownList))
prepareLocalItemsData =
L.zipWith $ \(ComposedMessage {msgContent = mc}, itemForwarded, ts, _) f ->
(CISndMsgContent mc, f, itemForwarded, ts)
getConnQueueInfo user Connection {connId, agentConnId = AgentConnId acId} = do
msgInfo <- withFastStore' (`getLastRcvMsgInfo` connId)
CRQueueInfo user msgInfo <$> withAgent (\a -> getConnectionQueueInfo a nm acId)
withSendRef :: ChatRef -> (SendRef -> CM ChatResponse) -> CM ChatResponse
withSendRef chatRef a = case chatRef of
ChatRef CTDirect cId _ -> a $ SRDirect cId
ChatRef CTGroup gId scope -> a $ SRGroup gId scope
_ -> throwCmdError "not supported"
getSharedMsgId :: CM SharedMsgId
getSharedMsgId = do
gVar <- asks random
liftIO $ SharedMsgId <$> encodedRandomBytes gVar 12
data ConnectViaContactResult
= CVRConnectedContact Contact
| CVRSentInvitation Connection (Maybe Profile)
protocolServers :: UserProtocol p => SProtocolType p -> ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) -> ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP])
protocolServers p (operators, smpServers, xftpServers) = case p of
SPSMP -> (operators, smpServers, [])
SPXFTP -> (operators, [], xftpServers)
-- disable preset and replace custom servers (groupByOperator always adds custom)
updatedServers :: forall p. UserProtocol p => SProtocolType p -> [AUserServer p] -> UserOperatorServers -> UpdatedUserOperatorServers
updatedServers p' srvs UserOperatorServers {operator, smpServers, xftpServers} = case p' of
SPSMP -> u (updateSrvs smpServers, map (AUS SDBStored) xftpServers)
SPXFTP -> u (map (AUS SDBStored) smpServers, updateSrvs xftpServers)
where
u = uncurry $ UpdatedUserOperatorServers operator
updateSrvs :: [UserServer p] -> [AUserServer p]
updateSrvs pSrvs = map disableSrv pSrvs <> maybe srvs (const []) operator
disableSrv srv@UserServer {preset} =
AUS SDBStored $ if preset then srv {enabled = False} else srv {deleted = True}
type ComposedMessageReq = (ComposedMessage, Maybe CIForwardedFrom, (Text, Maybe MarkdownList), Map MemberName CIMention)
composedMessage :: Maybe CryptoFile -> MsgContent -> ComposedMessage
composedMessage f mc = ComposedMessage {fileSource = f, quotedItemId = Nothing, msgContent = mc, mentions = M.empty}
composedMessageReq :: ComposedMessage -> ComposedMessageReq
composedMessageReq cm@ComposedMessage {msgContent = mc} = (cm, Nothing, msgContentTexts mc, M.empty)
composedMessageReqMentions :: DB.Connection -> User -> GroupInfo -> ComposedMessage -> ExceptT StoreError IO ComposedMessageReq
composedMessageReqMentions db user g cm@ComposedMessage {msgContent = mc, mentions} = do
let ts@(_, ft_) = msgContentTexts mc
(cm,Nothing,ts,) <$> getCIMentions db user g ft_ mentions
data ChangedProfileContact = ChangedProfileContact
{ ct :: Contact,
ct' :: Contact,
mergedProfile' :: Profile,
conn :: Connection
}
createContactsSndFeatureItems :: User -> [ChangedProfileContact] -> CM' ()
createContactsSndFeatureItems user cts =
createContactsFeatureItems user cts' CDDirectSnd CISndChatFeature CISndChatPreference getPref
where
cts' = map (\ChangedProfileContact {ct, ct'} -> (ct, ct')) cts
getPref ContactUserPreference {userPreference} = case userPreference of
CUPContact {preference} -> preference
CUPUser {preference} -> preference
assertDirectAllowed :: User -> MsgDirection -> Contact -> CMEventTag e -> CM ()
assertDirectAllowed user dir ct event =
unless (allowedChatEvent || anyDirectOrUsed ct) . unlessM directMessagesAllowed $
throwChatError (CEDirectMessagesProhibited dir ct)
where
directMessagesAllowed = any (uncurry $ groupFeatureMemberAllowed' SGFDirectMessages) <$> withStore' (\db -> getContactGroupPreferences db user ct)
allowedChatEvent = case event of
XMsgNew_ -> False
XMsgUpdate_ -> False
XMsgDel_ -> False
XFile_ -> False
XGrpInv_ -> False
XCallInv_ -> False
_ -> True
startExpireCIThread :: User -> CM' ()
startExpireCIThread user@User {userId} = do
expireThreads <- asks expireCIThreads
atomically (TM.lookup userId expireThreads) >>= \case
Nothing -> do
a <- Just <$> async runExpireCIs
atomically $ TM.insert userId a expireThreads
_ -> pure ()
where
runExpireCIs = do
delay <- asks (initialCleanupManagerDelay . config)
liftIO $ threadDelay' delay
interval <- asks $ ciExpirationInterval . config
forever $ do
flip catchAllErrors' (eToView') $ do
expireFlags <- asks expireCIFlags
atomically $ TM.lookup userId expireFlags >>= \b -> unless (b == Just True) retry
lift waitChatStartedAndActivated
ttl <- withStore' (`getChatItemTTL` user)
expireChatItems user ttl False
liftIO $ threadDelay' interval
setChatItemsExpiration :: User -> Int64 -> Int -> CM' ()
setChatItemsExpiration user newTTL ttlCount
| newTTL > 0 || ttlCount > 0 = do
startExpireCIThread user
whenM chatStarted $ setExpireCIFlag user True
| otherwise = setExpireCIFlag user False
setExpireCIFlag :: User -> Bool -> CM' ()
setExpireCIFlag User {userId} b = do
expireFlags <- asks expireCIFlags
atomically $ TM.insert userId b expireFlags
setAllExpireCIFlags :: Bool -> CM' ()
setAllExpireCIFlags b = do
expireFlags <- asks expireCIFlags
atomically $ do
keys <- M.keys <$> readTVar expireFlags
forM_ keys $ \k -> TM.insert k b expireFlags
agentSubscriber :: CM' ()
agentSubscriber = do
q <- asks $ subQ . smpAgent
forever (atomically (readTBQueue q) >>= process)
`E.catchAny` \e -> do
eToView' $ ChatErrorAgent (CRITICAL True $ "Message reception stopped: " <> show e) (AgentConnId "") Nothing
E.throwIO e
where
process :: (ACorrId, AEntityId, AEvt) -> CM' ()
process (corrId, entId, AEvt e msg) = run $ case e of
SAENone -> processAgentMessageNoConn msg
SAEConn -> processAgentMessage corrId entId msg
SAERcvFile -> processAgentMsgRcvFile corrId entId msg
SAESndFile -> processAgentMsgSndFile corrId entId msg
where
run action = action `catchAllErrors'` (eToView')
type AgentSubResult = Map ConnId (Either AgentErrorType (Maybe ClientServiceId))
cleanupManager :: CM ()
cleanupManager = do
interval <- asks (cleanupManagerInterval . config)
runWithoutInitialDelay interval
initialDelay <- asks (initialCleanupManagerDelay . config)
liftIO $ threadDelay' initialDelay
stepDelay <- asks (cleanupManagerStepDelay . config)
forever $ do
flip catchAllErrors eToView $ do
lift waitChatStartedAndActivated
users <- withStore' getUsers
let (us, us') = partition activeUser users
forM_ us $ cleanupUser interval stepDelay
forM_ us' $ cleanupUser interval stepDelay
cleanupMessages `catchAllErrors` eToView
cleanupDeliveryTasks `catchAllErrors` eToView
cleanupDeliveryJobs `catchAllErrors` eToView
-- TODO possibly, also cleanup async commands
cleanupProbes `catchAllErrors` eToView
liftIO $ threadDelay' $ diffToMicroseconds interval
where
runWithoutInitialDelay cleanupInterval = flip catchAllErrors eToView $ do
lift waitChatStartedAndActivated
users <- withStore' getUsers
let (us, us') = partition activeUser users
forM_ us $ \u -> cleanupTimedItems cleanupInterval u `catchAllErrors` eToView
forM_ us' $ \u -> cleanupTimedItems cleanupInterval u `catchAllErrors` eToView
cleanupUser cleanupInterval stepDelay user = do
cleanupTimedItems cleanupInterval user `catchAllErrors` eToView
liftIO $ threadDelay' stepDelay
-- TODO remove in future versions: legacy step - contacts are no longer marked as deleted
cleanupDeletedContacts user `catchAllErrors` eToView
liftIO $ threadDelay' stepDelay
cleanupTimedItems cleanupInterval user = do
ts <- liftIO getCurrentTime
let startTimedThreadCutoff = addUTCTime cleanupInterval ts
timedItems <- withStore' $ \db -> getTimedItems db user startTimedThreadCutoff
forM_ timedItems $ \(itemRef, deleteAt) -> startTimedItemThread user itemRef deleteAt `catchAllErrors` const (pure ())
cleanupDeletedContacts user = do
vr <- chatVersionRange
contacts <- withStore' $ \db -> getDeletedContacts db vr user
forM_ contacts $ \ct ->
withStore (\db -> deleteContactWithoutGroups db user ct)
`catchAllErrors` eToView
cleanupMessages = do
ts <- liftIO getCurrentTime
let cutoffTs = addUTCTime (-(30 * nominalDay)) ts
withStore' (`deleteOldMessages` cutoffTs)
cleanupDeliveryTasks = do
ts <- liftIO getCurrentTime
let cutoffTs = addUTCTime (-(7 * nominalDay)) ts
withStore' (`deleteDoneDeliveryTasks` cutoffTs)
cleanupDeliveryJobs = do
ts <- liftIO getCurrentTime
let cutoffTs = addUTCTime (-(7 * nominalDay)) ts
withStore' (`deleteDoneDeliveryJobs` cutoffTs)
cleanupProbes = do
ts <- liftIO getCurrentTime
let cutoffTs = addUTCTime (-(14 * nominalDay)) ts
withStore' (`deleteOldProbes` cutoffTs)
expireChatItems :: User -> Int64 -> Bool -> CM ()
expireChatItems user@User {userId} globalTTL sync = do
currentTs <- liftIO getCurrentTime
vr <- chatVersionRange
-- this is to keep group messages created during last 12 hours even if they're expired according to item_ts
let createdAtCutoff = addUTCTime (-43200 :: NominalDiffTime) currentTs
lift waitChatStartedAndActivated
contactIds <- withStore' $ \db -> getUserContactsToExpire db user globalTTL
loop contactIds $ expireContactChatItems user vr globalTTL
lift waitChatStartedAndActivated
groupIds <- withStore' $ \db -> getUserGroupsToExpire db user globalTTL
loop groupIds $ expireGroupChatItems user vr globalTTL createdAtCutoff
where
loop :: [Int64] -> (Int64 -> CM ()) -> CM ()
loop [] _ = pure ()
loop (a : as) process = continue $ do
process a `catchAllErrors` eToView
loop as process
continue :: CM () -> CM ()
continue a =
if sync
then a
else do
expireFlags <- asks expireCIFlags
expire <- atomically $ TM.lookup userId expireFlags
when (expire == Just True) $ threadDelay 100000 >> a
expireContactChatItems :: User -> VersionRangeChat -> Int64 -> ContactId -> CM ()
expireContactChatItems user vr globalTTL ctId =
-- reading contacts and groups inside the loop,
-- to allow ttl changing while processing and to reduce memory usage
tryAllErrors (withStore $ \db -> getContact db vr user ctId) >>= mapM_ process
where
process ct@Contact {chatItemTTL} =
withExpirationDate globalTTL chatItemTTL $ \expirationDate -> do
lift waitChatStartedAndActivated
filesInfo <- withStore' $ \db -> getContactExpiredFileInfo db user ct expirationDate
deleteCIFiles user filesInfo
withStore' $ \db -> deleteContactExpiredCIs db user ct expirationDate
expireGroupChatItems :: User -> VersionRangeChat -> Int64 -> UTCTime -> GroupId -> CM ()
expireGroupChatItems user vr globalTTL createdAtCutoff groupId =
tryAllErrors (withStore $ \db -> getGroupInfo db vr user groupId) >>= mapM_ process
where
process gInfo@GroupInfo {chatItemTTL} =
withExpirationDate globalTTL chatItemTTL $ \expirationDate -> do
lift waitChatStartedAndActivated
filesInfo <- withStore' $ \db -> getGroupExpiredFileInfo db user gInfo expirationDate createdAtCutoff
deleteCIFiles user filesInfo
withStore' $ \db -> deleteGroupExpiredCIs db user gInfo expirationDate createdAtCutoff
membersToDelete <- withStore' $ \db -> getGroupMembersForExpiration db vr user gInfo
forM_ membersToDelete $ \m -> withStore' $ \db -> deleteGroupMember db user m
withExpirationDate :: Int64 -> Maybe Int64 -> (UTCTime -> CM ()) -> CM ()
withExpirationDate globalTTL chatItemTTL action = do
currentTs <- liftIO getCurrentTime
let ttl = fromMaybe globalTTL chatItemTTL
when (ttl > 0) $ action $ addUTCTime (-1 * fromIntegral ttl) currentTs
chatCommandP :: Parser ChatCommand
chatCommandP =
choice
[ "/mute " *> ((`SetShowMessages` MFNone) <$> chatNameP),
"/unmute " *> ((`SetShowMessages` MFAll) <$> chatNameP),
"/unmute mentions " *> ((`SetShowMessages` MFMentions) <$> chatNameP),
"/receipts " *> (SetSendReceipts <$> chatNameP <* " " <*> ((Just <$> onOffP) <|> ("default" $> Nothing))),
"/block #" *> (SetShowMemberMessages <$> displayNameP <* A.space <*> (char_ '@' *> displayNameP) <*> pure False),
"/unblock #" *> (SetShowMemberMessages <$> displayNameP <* A.space <*> (char_ '@' *> displayNameP) <*> pure True),
"/_create user " *> (CreateActiveUser <$> jsonP),
"/create user " *> (CreateActiveUser <$> newUserP),
"/create bot " *> (CreateActiveUser <$> newBotUserP),
"/users" $> ListUsers,
"/_user " *> (APISetActiveUser <$> A.decimal <*> optional (A.space *> jsonP)),
("/user " <|> "/u ") *> (SetActiveUser <$> displayNameP <*> optional (A.space *> pwdP)),
"/set receipts all " *> (SetAllContactReceipts <$> onOffP),
"/_set receipts contacts " *> (APISetUserContactReceipts <$> A.decimal <* A.space <*> receiptSettings),
"/set receipts contacts " *> (SetUserContactReceipts <$> receiptSettings),
"/_set receipts groups " *> (APISetUserGroupReceipts <$> A.decimal <* A.space <*> receiptSettings),
"/set receipts groups " *> (SetUserGroupReceipts <$> receiptSettings),
"/_set accept member contacts " *> (APISetUserAutoAcceptMemberContacts <$> A.decimal <* A.space <*> onOffP),
"/set accept member contacts " *> (SetUserAutoAcceptMemberContacts <$> onOffP),
"/_hide user " *> (APIHideUser <$> A.decimal <* A.space <*> jsonP),
"/_unhide user " *> (APIUnhideUser <$> A.decimal <* A.space <*> jsonP),
"/_mute user " *> (APIMuteUser <$> A.decimal),
"/_unmute user " *> (APIUnmuteUser <$> A.decimal),
"/hide user " *> (HideUser <$> pwdP),
"/unhide user " *> (UnhideUser <$> pwdP),
"/mute user" $> MuteUser,
"/unmute user" $> UnmuteUser,
"/_delete user " *> (APIDeleteUser <$> A.decimal <* " del_smp=" <*> onOffP <*> optional (A.space *> jsonP)),
"/delete user " *> (DeleteUser <$> displayNameP <*> pure True <*> optional (A.space *> pwdP)),
("/user" <|> "/u") $> ShowActiveUser,
"/_start " *> do
mainApp <- "main=" *> onOffP
enableSndFiles <- " snd_files=" *> onOffP <|> pure mainApp
pure StartChat {mainApp, enableSndFiles},
"/_start" $> StartChat {mainApp = True, enableSndFiles = True},
"/_check running" $> CheckChatRunning,
"/_stop" $> APIStopChat,
"/_app activate restore=" *> (APIActivateChat <$> onOffP),
"/_app activate" $> APIActivateChat True,
"/_app suspend " *> (APISuspendChat <$> A.decimal),
"/_connections diff" *> (ShowConnectionsDiff <$> (" show_ids=" *> onOffP <|> pure False)),
"/_resubscribe all" $> ResubscribeAllConnections,
-- deprecated, use /set file paths
"/_temp_folder " *> (SetTempFolder <$> filePath),
-- /_files_folder deprecated, use /set file paths
("/_files_folder " <|> "/files_folder ") *> (SetFilesFolder <$> filePath),
-- deprecated, use /set file paths
"/remote_hosts_folder " *> (SetRemoteHostsFolder <$> filePath),
"/set file paths " *> (APISetAppFilePaths <$> jsonP),
"/_files_encrypt " *> (APISetEncryptLocalFiles <$> onOffP),
"/contact_merge " *> (SetContactMergeEnabled <$> onOffP),
#if !defined(dbPostgres)
"/_db export " *> (APIExportArchive <$> jsonP),
"/db export" $> ExportArchive,
"/_db import " *> (APIImportArchive <$> jsonP),
"/_db delete" $> APIDeleteStorage,
"/_db encryption " *> (APIStorageEncryption <$> jsonP),
"/db encrypt " *> (APIStorageEncryption . dbEncryptionConfig "" <$> dbKeyP),
"/db key " *> (APIStorageEncryption <$> (dbEncryptionConfig <$> dbKeyP <* A.space <*> dbKeyP)),
"/db decrypt " *> (APIStorageEncryption . (`dbEncryptionConfig` "") <$> dbKeyP),
"/db test key " *> (TestStorageEncryption <$> dbKeyP),
"/sql slow" $> SlowSQLQueries,
#endif
"/_save app settings" *> (APISaveAppSettings <$> jsonP),
"/_get app settings" *> (APIGetAppSettings <$> optional (A.space *> jsonP)),
"/sql chat " *> (ExecChatStoreSQL <$> textP),
"/sql agent " *> (ExecAgentStoreSQL <$> textP),
"/_get tags " *> (APIGetChatTags <$> A.decimal),
"/_get chats "
*> ( APIGetChats
<$> A.decimal
<*> (" pcc=on" $> True <|> " pcc=off" $> False <|> pure False)
<*> (A.space *> paginationByTimeP <|> pure (PTLast 5000))
<*> (A.space *> jsonP <|> pure clqNoFilters)
),
"/_get chat " *> (APIGetChat <$> chatRefP <*> optional (" content=" *> strP) <* A.space <*> chatPaginationP <*> optional (" search=" *> textP)),
"/_get items " *> (APIGetChatItems <$> chatPaginationP <*> optional (" search=" *> textP)),
"/_get item info " *> (APIGetChatItemInfo <$> chatRefP <* A.space <*> A.decimal),
"/_send " *> (APISendMessages <$> sendRefP <*> liveMessageP <*> sendMessageTTLP <*> (" json " *> jsonP <|> " text " *> composedMessagesTextP)),
"/_create tag " *> (APICreateChatTag <$> jsonP),
"/_tags " *> (APISetChatTags <$> chatRefP <*> optional _strP),
"/_delete tag " *> (APIDeleteChatTag <$> A.decimal),
"/_update tag " *> (APIUpdateChatTag <$> A.decimal <* A.space <*> jsonP),
"/_reorder tags " *> (APIReorderChatTags <$> strP),
"/_create *" *> (APICreateChatItems <$> A.decimal <*> (" json " *> jsonP <|> " text " *> composedMessagesTextP)),
"/_report #" *> (APIReportMessage <$> A.decimal <* A.space <*> A.decimal <*> (" reason=" *> strP) <*> (A.space *> textP <|> pure "")),
"/report #" *> (ReportMessage <$> displayNameP <*> optional (" @" *> displayNameP) <*> _strP <* A.space <*> msgTextP),
"/_update item " *> (APIUpdateChatItem <$> chatRefP <* A.space <*> A.decimal <*> liveMessageP <*> (" json" *> jsonP <|> " text " *> updatedMessagesTextP)),
"/_delete item " *> (APIDeleteChatItem <$> chatRefP <*> _strP <*> _strP),
"/_delete member item #" *> (APIDeleteMemberChatItem <$> A.decimal <*> _strP),
"/_archive reports #" *> (APIArchiveReceivedReports <$> A.decimal),
"/_delete reports #" *> (APIDeleteReceivedReports <$> A.decimal <*> _strP <*> _strP),
"/_reaction " *> (APIChatItemReaction <$> chatRefP <* A.space <*> A.decimal <* A.space <*> onOffP <* A.space <*> (knownReaction <$?> jsonP)),
"/_reaction members " *> (APIGetReactionMembers <$> A.decimal <* " #" <*> A.decimal <* A.space <*> A.decimal <* A.space <*> (knownReaction <$?> jsonP)),
"/_forward plan " *> (APIPlanForwardChatItems <$> chatRefP <*> _strP),
"/_forward " *> (APIForwardChatItems <$> chatRefP <* A.space <*> chatRefP <*> _strP <*> sendMessageTTLP),
"/_read user " *> (APIUserRead <$> A.decimal),
"/read user" $> UserRead,
"/_read chat " *> (APIChatRead <$> chatRefP),
"/_read chat items " *> (APIChatItemsRead <$> chatRefP <*> _strP),
"/_unread chat " *> (APIChatUnread <$> chatRefP <* A.space <*> onOffP),
"/_delete " *> (APIDeleteChat <$> chatRefP <*> chatDeleteMode),
"/_clear chat " *> (APIClearChat <$> chatRefP),
"/_accept" *> (APIAcceptContact <$> incognitoOnOffP <* A.space <*> A.decimal),
"/_reject " *> (APIRejectContact <$> A.decimal),
"/_call invite @" *> (APISendCallInvitation <$> A.decimal <* A.space <*> jsonP),
"/call " *> char_ '@' *> (SendCallInvitation <$> displayNameP <*> pure defaultCallType),
"/_call reject @" *> (APIRejectCall <$> A.decimal),
"/_call offer @" *> (APISendCallOffer <$> A.decimal <* A.space <*> jsonP),
"/_call answer @" *> (APISendCallAnswer <$> A.decimal <* A.space <*> jsonP),
"/_call extra @" *> (APISendCallExtraInfo <$> A.decimal <* A.space <*> jsonP),
"/_call end @" *> (APIEndCall <$> A.decimal),
"/_call status @" *> (APICallStatus <$> A.decimal <* A.space <*> strP),
"/_call get" $> APIGetCallInvitations,
"/_profile " *> (APIUpdateProfile <$> A.decimal <* A.space <*> jsonP),
"/_set alias @" *> (APISetContactAlias <$> A.decimal <*> (A.space *> textP <|> pure "")),
"/_set alias #" *> (APISetGroupAlias <$> A.decimal <*> (A.space *> textP <|> pure "")),
"/_set alias :" *> (APISetConnectionAlias <$> A.decimal <*> (A.space *> textP <|> pure "")),
"/_set prefs @" *> (APISetContactPrefs <$> A.decimal <* A.space <*> jsonP),
"/_set theme user " *> (APISetUserUIThemes <$> A.decimal <*> optional (A.space *> jsonP)),
"/_set theme " *> (APISetChatUIThemes <$> chatRefP <*> optional (A.space *> jsonP)),
"/_ntf get" $> APIGetNtfToken,
"/_ntf register " *> (APIRegisterToken <$> strP_ <*> strP),
"/_ntf verify " *> (APIVerifyToken <$> strP <* A.space <*> strP <* A.space <*> strP),
"/_ntf check " *> (APICheckToken <$> strP),
"/_ntf delete " *> (APIDeleteToken <$> strP),
"/_ntf conns " *> (APIGetNtfConns <$> strP <* A.space <*> strP),
"/_ntf conn messages " *> (APIGetConnNtfMessages <$> connMsgsP),
"/_add #" *> (APIAddMember <$> A.decimal <* A.space <*> A.decimal <*> memberRole),
"/_join #" *> (APIJoinGroup <$> A.decimal <*> pure MFAll), -- needs to be changed to support in UI
"/_accept member #" *> (APIAcceptMember <$> A.decimal <* A.space <*> A.decimal <*> memberRole),
"/_delete member chat #" *> (APIDeleteMemberSupportChat <$> A.decimal <* A.space <*> A.decimal),
"/_member role #" *> (APIMembersRole <$> A.decimal <*> _strP <*> memberRole),
"/_block #" *> (APIBlockMembersForAll <$> A.decimal <*> _strP <* " blocked=" <*> onOffP),
"/_remove #" *> (APIRemoveMembers <$> A.decimal <*> _strP <*> (" messages=" *> onOffP <|> pure False)),
"/_leave #" *> (APILeaveGroup <$> A.decimal),
"/_members #" *> (APIListMembers <$> A.decimal),
-- "/_archive conversations #" *> (APIArchiveGroupConversations <$> A.decimal <*> _strP),
-- "/_delete conversations #" *> (APIDeleteGroupConversations <$> A.decimal <*> _strP),
"/_server test " *> (APITestProtoServer <$> A.decimal <* A.space <*> strP),
"/smp test " *> (TestProtoServer . AProtoServerWithAuth SPSMP <$> strP),
"/xftp test " *> (TestProtoServer . AProtoServerWithAuth SPXFTP <$> strP),
"/ntf test " *> (TestProtoServer . AProtoServerWithAuth SPNTF <$> strP),
"/smp " *> (SetUserProtoServers (AProtocolType SPSMP) . map (AProtoServerWithAuth SPSMP) <$> protocolServersP),
"/xftp " *> (SetUserProtoServers (AProtocolType SPXFTP) . map (AProtoServerWithAuth SPXFTP) <$> protocolServersP),
"/smp" $> GetUserProtoServers (AProtocolType SPSMP),
"/xftp" $> GetUserProtoServers (AProtocolType SPXFTP),
"/_operators" $> APIGetServerOperators,
"/_operators " *> (APISetServerOperators <$> jsonP),
"/operators " *> (SetServerOperators . L.fromList <$> operatorRolesP `A.sepBy1` A.char ','),
"/_servers " *> (APIGetUserServers <$> A.decimal),
"/_servers " *> (APISetUserServers <$> A.decimal <* A.space <*> jsonP),
"/_validate_servers " *> (APIValidateServers <$> A.decimal <* A.space <*> jsonP),
"/_conditions" $> APIGetUsageConditions,
"/_conditions_notified " *> (APISetConditionsNotified <$> A.decimal),
"/_accept_conditions " *> (APIAcceptConditions <$> A.decimal <*> _strP),
"/_ttl " *> (APISetChatItemTTL <$> A.decimal <* A.space <*> A.decimal),
"/_ttl " *> (APISetChatTTL <$> A.decimal <* A.space <*> chatRefP <* A.space <*> ciTTLDecimal),
"/_ttl " *> (APIGetChatItemTTL <$> A.decimal),
"/ttl " *> (SetChatItemTTL <$> ciTTL),
"/ttl" $> GetChatItemTTL,
"/ttl " *> (SetChatTTL <$> chatNameP <* A.space <*> (("default" $> Nothing) <|> (Just <$> ciTTL))),
"/ttl " *> (GetChatTTL <$> chatNameP),
"/_network info " *> (APISetNetworkInfo <$> jsonP),
"/_network " *> (APISetNetworkConfig <$> jsonP),
("/network " <|> "/net ") *> (SetNetworkConfig <$> netCfgP),
("/network" <|> "/net") $> APIGetNetworkConfig,
"/reconnect " *> (ReconnectServer <$> A.decimal <* A.space <*> strP),
"/reconnect" $> ReconnectAllServers,
"/_settings " *> (APISetChatSettings <$> chatRefP <* A.space <*> jsonP),
"/_member settings #" *> (APISetMemberSettings <$> A.decimal <* A.space <*> A.decimal <* A.space <*> jsonP),
"/_info #" *> (APIGroupMemberInfo <$> A.decimal <* A.space <*> A.decimal),
"/_info #" *> (APIGroupInfo <$> A.decimal),
"/_info @" *> (APIContactInfo <$> A.decimal),
("/info #" <|> "/i #") *> (GroupMemberInfo <$> displayNameP <* A.space <* char_ '@' <*> displayNameP),
("/info #" <|> "/i #") *> (ShowGroupInfo <$> displayNameP),
("/info " <|> "/i ") *> char_ '@' *> (ContactInfo <$> displayNameP),
"/_queue info #" *> (APIGroupMemberQueueInfo <$> A.decimal <* A.space <*> A.decimal),
"/_queue info @" *> (APIContactQueueInfo <$> A.decimal),
("/queue info #" <|> "/qi #") *> (GroupMemberQueueInfo <$> displayNameP <* A.space <* char_ '@' <*> displayNameP),
("/queue info " <|> "/qi ") *> char_ '@' *> (ContactQueueInfo <$> displayNameP),
"/_switch #" *> (APISwitchGroupMember <$> A.decimal <* A.space <*> A.decimal),
"/_switch @" *> (APISwitchContact <$> A.decimal),
"/_abort switch #" *> (APIAbortSwitchGroupMember <$> A.decimal <* A.space <*> A.decimal),
"/_abort switch @" *> (APIAbortSwitchContact <$> A.decimal),
"/_sync #" *> (APISyncGroupMemberRatchet <$> A.decimal <* A.space <*> A.decimal <*> (" force=on" $> True <|> pure False)),
"/_sync @" *> (APISyncContactRatchet <$> A.decimal <*> (" force=on" $> True <|> pure False)),
"/switch #" *> (SwitchGroupMember <$> displayNameP <* A.space <* char_ '@' <*> displayNameP),
"/switch " *> char_ '@' *> (SwitchContact <$> displayNameP),
"/abort switch #" *> (AbortSwitchGroupMember <$> displayNameP <* A.space <* char_ '@' <*> displayNameP),
"/abort switch " *> char_ '@' *> (AbortSwitchContact <$> displayNameP),
"/sync #" *> (SyncGroupMemberRatchet <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <*> (" force=on" $> True <|> pure False)),
"/sync " *> char_ '@' *> (SyncContactRatchet <$> displayNameP <*> (" force=on" $> True <|> pure False)),
"/_get code @" *> (APIGetContactCode <$> A.decimal),
"/_get code #" *> (APIGetGroupMemberCode <$> A.decimal <* A.space <*> A.decimal),
"/_verify code @" *> (APIVerifyContact <$> A.decimal <*> optional (A.space *> verifyCodeP)),
"/_verify code #" *> (APIVerifyGroupMember <$> A.decimal <* A.space <*> A.decimal <*> optional (A.space *> verifyCodeP)),
"/_enable @" *> (APIEnableContact <$> A.decimal),
"/_enable #" *> (APIEnableGroupMember <$> A.decimal <* A.space <*> A.decimal),
"/code " *> char_ '@' *> (GetContactCode <$> displayNameP),
"/code #" *> (GetGroupMemberCode <$> displayNameP <* A.space <* char_ '@' <*> displayNameP),
"/verify " *> char_ '@' *> (VerifyContact <$> displayNameP <*> optional (A.space *> verifyCodeP)),
"/verify #" *> (VerifyGroupMember <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <*> optional (A.space *> verifyCodeP)),
"/enable " *> char_ '@' *> (EnableContact <$> displayNameP),
"/enable #" *> (EnableGroupMember <$> displayNameP <* A.space <* char_ '@' <*> displayNameP),
("/help files" <|> "/help file" <|> "/hf") $> ChatHelp HSFiles,
("/help groups" <|> "/help group" <|> "/hg") $> ChatHelp HSGroups,
("/help contacts" <|> "/help contact" <|> "/hc") $> ChatHelp HSContacts,
("/help address" <|> "/ha") $> ChatHelp HSMyAddress,
("/help incognito" <|> "/hi") $> ChatHelp HSIncognito,
("/help messages" <|> "/hm") $> ChatHelp HSMessages,
("/help remote" <|> "/hr") $> ChatHelp HSRemote,
("/help settings" <|> "/hs") $> ChatHelp HSSettings,
("/help db" <|> "/hd") $> ChatHelp HSDatabase,
("/help" <|> "/h") $> ChatHelp HSMain,
("/group" <|> "/g") *> (NewGroup <$> incognitoP <* A.space <* char_ '#' <*> groupProfile),
"/_group " *> (APINewGroup <$> A.decimal <*> incognitoOnOffP <* A.space <*> jsonP),
("/add " <|> "/a ") *> char_ '#' *> (AddMember <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <*> (memberRole <|> pure GRMember)),
("/join " <|> "/j ") *> char_ '#' *> (JoinGroup <$> displayNameP <*> (" mute" $> MFNone <|> pure MFAll)),
"/accept member " *> char_ '#' *> (AcceptMember <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <*> (memberRole <|> pure GRMember)),
("/member role " <|> "/mr ") *> char_ '#' *> (MemberRole <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <*> memberRole),
"/block for all #" *> (BlockForAll <$> displayNameP <* A.space <*> (char_ '@' *> displayNameP) <*> pure True),
"/unblock for all #" *> (BlockForAll <$> displayNameP <* A.space <*> (char_ '@' *> displayNameP) <*> pure False),
("/remove " <|> "/rm ") *> char_ '#' *> (RemoveMembers <$> displayNameP <* A.space <*> (L.fromList <$> (char_ '@' *> displayNameP) `A.sepBy1'` A.char ',') <*> (" messages=" *> onOffP <|> pure False)),
("/leave " <|> "/l ") *> char_ '#' *> (LeaveGroup <$> displayNameP),
("/delete #" <|> "/d #") *> (DeleteGroup <$> displayNameP),
("/delete " <|> "/d ") *> char_ '@' *> (DeleteContact <$> displayNameP <*> chatDeleteMode),
"/clear *" $> ClearNoteFolder,
"/clear #" *> (ClearGroup <$> displayNameP),
"/clear " *> char_ '@' *> (ClearContact <$> displayNameP),
("/members " <|> "/ms ") *> char_ '#' *> (ListMembers <$> displayNameP),
"/member support chats #" *> (ListMemberSupportChats <$> displayNameP),
"/_groups" *> (APIListGroups <$> A.decimal <*> optional (" @" *> A.decimal) <*> optional (A.space *> textP)),
("/groups" <|> "/gs") *> (ListGroups <$> optional (" @" *> displayNameP) <*> optional (A.space *> textP)),
"/_group_profile #" *> (APIUpdateGroupProfile <$> A.decimal <* A.space <*> jsonP),
("/group_profile " <|> "/gp ") *> char_ '#' *> (UpdateGroupNames <$> displayNameP <* A.space <*> groupProfile),
("/group_profile " <|> "/gp ") *> char_ '#' *> (ShowGroupProfile <$> displayNameP),
"/group_descr " *> char_ '#' *> (UpdateGroupDescription <$> displayNameP <*> optional (A.space *> msgTextP)),
"/set welcome " *> char_ '#' *> (UpdateGroupDescription <$> displayNameP <* A.space <*> (Just <$> msgTextP)),
"/delete welcome " *> char_ '#' *> (UpdateGroupDescription <$> displayNameP <*> pure Nothing),
"/show welcome " *> char_ '#' *> (ShowGroupDescription <$> displayNameP),
"/_create link #" *> (APICreateGroupLink <$> A.decimal <*> (memberRole <|> pure GRMember)),
"/_set link role #" *> (APIGroupLinkMemberRole <$> A.decimal <*> memberRole),
"/_delete link #" *> (APIDeleteGroupLink <$> A.decimal),
"/_get link #" *> (APIGetGroupLink <$> A.decimal),
"/_short link #" *> (APIAddGroupShortLink <$> A.decimal),
"/create link #" *> (CreateGroupLink <$> displayNameP <*> (memberRole <|> pure GRMember)),
"/set link role #" *> (GroupLinkMemberRole <$> displayNameP <*> memberRole),
"/delete link #" *> (DeleteGroupLink <$> displayNameP),
"/show link #" *> (ShowGroupLink <$> displayNameP),
"/_create member contact #" *> (APICreateMemberContact <$> A.decimal <* A.space <*> A.decimal),
"/_invite member contact @" *> (APISendMemberContactInvitation <$> A.decimal <*> optional (A.space *> msgContentP)),
"/_accept member contact @" *> (APIAcceptMemberContact <$> A.decimal),
(">#" <|> "> #") *> (SendGroupMessageQuote <$> displayNameP <* A.space <*> pure Nothing <*> quotedMsg <*> msgTextP),
(">#" <|> "> #") *> (SendGroupMessageQuote <$> displayNameP <* A.space <* char_ '@' <*> (Just <$> displayNameP) <* A.space <*> quotedMsg <*> msgTextP),
"/_contacts " *> (APIListContacts <$> A.decimal),
"/contacts" $> ListContacts,
"/_connect plan " *> (APIConnectPlan <$> A.decimal <* A.space <*> ((Just <$> strP) <|> A.takeTill (== ' ') $> Nothing)),
"/_prepare contact " *> (APIPrepareContact <$> A.decimal <* A.space <*> connLinkP <* A.space <*> jsonP),
"/_prepare group " *> (APIPrepareGroup <$> A.decimal <* A.space <*> connLinkP' <* A.space <*> jsonP),
"/_set contact user @" *> (APIChangePreparedContactUser <$> A.decimal <* A.space <*> A.decimal),
"/_set group user #" *> (APIChangePreparedGroupUser <$> A.decimal <* A.space <*> A.decimal),
"/_connect contact @" *> (APIConnectPreparedContact <$> A.decimal <*> incognitoOnOffP <*> optional (A.space *> msgContentP)),
"/_connect group #" *> (APIConnectPreparedGroup <$> A.decimal <*> incognitoOnOffP <*> optional (A.space *> msgContentP)),
"/_connect " *> (APIAddContact <$> A.decimal <*> incognitoOnOffP),
"/_connect " *> (APIConnect <$> A.decimal <*> incognitoOnOffP <* A.space <*> connLinkP_),
"/_set incognito :" *> (APISetConnectionIncognito <$> A.decimal <* A.space <*> onOffP),
"/_set conn user :" *> (APIChangeConnectionUser <$> A.decimal <* A.space <*> A.decimal),
("/connect" <|> "/c") *> (AddContact <$> incognitoP),
("/connect" <|> "/c") *> (Connect <$> incognitoP <* A.space <*> ((Just <$> strP) <|> A.takeTill isSpace $> Nothing)),
ForwardMessage <$> chatNameP <* " <- @" <*> displayNameP <* A.space <*> msgTextP,
ForwardGroupMessage <$> chatNameP <* " <- #" <*> displayNameP <* A.space <* A.char '@' <*> (Just <$> displayNameP) <* A.space <*> msgTextP,
ForwardGroupMessage <$> chatNameP <* " <- #" <*> displayNameP <*> pure Nothing <* A.space <*> msgTextP,
ForwardLocalMessage <$> chatNameP <* " <- * " <*> msgTextP,
SendMessage <$> sendNameP <* A.space <*> msgTextP,
"@#" *> (SendMemberContactMessage <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <* A.space <*> msgTextP),
"/accept_member_contact @" *> (AcceptMemberContact <$> displayNameP),
"/live " *> (SendLiveMessage <$> chatNameP <*> (A.space *> msgTextP <|> pure "")),
(">@" <|> "> @") *> sendMsgQuote (AMsgDirection SMDRcv),
(">>@" <|> ">> @") *> sendMsgQuote (AMsgDirection SMDSnd),
("\\ " <|> "\\") *> (DeleteMessage <$> chatNameP <* A.space <*> textP),
("\\\\ #" <|> "\\\\#") *> (DeleteMemberMessage <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <* A.space <*> textP),
("! " <|> "!") *> (EditMessage <$> chatNameP <* A.space <*> (quotedMsg <|> pure "") <*> msgTextP),
ReactToMessage <$> (("+" $> True) <|> ("-" $> False)) <*> reactionP <* A.space <*> chatNameP' <* A.space <*> textP,
"/feed " *> (SendMessageBroadcast . MCText <$> msgTextP),
("/chats" <|> "/cs") *> (LastChats <$> (" all" $> Nothing <|> Just <$> (A.space *> A.decimal <|> pure 20))),
("/tail" <|> "/t") *> (LastMessages <$> optional (A.space *> chatNameP) <*> msgCountP <*> pure Nothing),
("/search" <|> "/?") *> (LastMessages <$> optional (A.space *> chatNameP) <*> msgCountP <*> (Just <$> (A.space *> textP))),
"/last_item_id" *> (LastChatItemId <$> optional (A.space *> chatNameP) <*> (A.space *> A.decimal <|> pure 0)),
"/show" *> (ShowLiveItems <$> (A.space *> onOffP <|> pure True)),
"/show " *> (ShowChatItem . Just <$> A.decimal),
"/item info " *> (ShowChatItemInfo <$> chatNameP <* A.space <*> msgTextP),
("/file " <|> "/f ") *> (SendFile <$> chatNameP' <* A.space <*> cryptoFileP),
("/image " <|> "/img ") *> (SendImage <$> chatNameP' <* A.space <*> cryptoFileP),
("/fforward " <|> "/ff ") *> (ForwardFile <$> chatNameP' <* A.space <*> A.decimal),
("/image_forward " <|> "/imgf ") *> (ForwardImage <$> chatNameP' <* A.space <*> A.decimal),
("/fdescription " <|> "/fd") *> (SendFileDescription <$> chatNameP' <* A.space <*> filePath),
("/freceive " <|> "/fr ") *> (ReceiveFile <$> A.decimal <*> (" approved_relays=" *> onOffP <|> pure False) <*> optional (" encrypt=" *> onOffP) <*> optional (" inline=" *> onOffP) <*> optional (A.space *> filePath)),
"/_set_file_to_receive " *> (SetFileToReceive <$> A.decimal <*> (" approved_relays=" *> onOffP <|> pure False) <*> optional (" encrypt=" *> onOffP)),
("/fcancel " <|> "/fc ") *> (CancelFile <$> A.decimal),
("/fstatus " <|> "/fs ") *> (FileStatus <$> A.decimal),
"/_connect contact " *> (APIConnectContactViaAddress <$> A.decimal <*> incognitoOnOffP <* A.space <*> A.decimal),
"/simplex" *> (ConnectSimplex <$> incognitoP),
"/_address " *> (APICreateMyAddress <$> A.decimal),
("/address" <|> "/ad") $> CreateMyAddress,
"/_delete_address " *> (APIDeleteMyAddress <$> A.decimal),
("/delete_address" <|> "/da") $> DeleteMyAddress,
"/_show_address " *> (APIShowMyAddress <$> A.decimal),
("/show_address" <|> "/sa") $> ShowMyAddress,
"/_short_link_address " *> (APIAddMyAddressShortLink <$> A.decimal),
"/_profile_address " *> (APISetProfileAddress <$> A.decimal <* A.space <*> onOffP),
("/profile_address " <|> "/pa ") *> (SetProfileAddress <$> onOffP),
"/_address_settings " *> (APISetAddressSettings <$> A.decimal <* A.space <*> jsonP),
"/auto_accept " *> (SetAddressSettings <$> autoAcceptP),
("/accept" <|> "/ac") *> (AcceptContact <$> incognitoP <* A.space <* char_ '@' <*> displayNameP),
("/reject " <|> "/rc ") *> char_ '@' *> (RejectContact <$> displayNameP),
("/markdown" <|> "/m") $> ChatHelp HSMarkdown,
("/welcome" <|> "/w") $> Welcome,
"/set profile image " *> (UpdateProfileImage . Just . ImageData <$> imageP),
"/delete profile image" $> UpdateProfileImage Nothing,
"/show profile image" $> ShowProfileImage,
("/profile " <|> "/p ") *> (uncurry UpdateProfile <$> profileNameDescr),
("/profile" <|> "/p") $> ShowProfile,
"/set bot commands " *> (SetBotCommands <$> botCommandsP),
"/delete bot commands" $> SetBotCommands [],
"/set voice #" *> (SetGroupFeatureRole (AGFR SGFVoice) <$> displayNameP <*> _strP <*> optional memberRole),
"/set voice @" *> (SetContactFeature (ACF SCFVoice) <$> displayNameP <*> optional (A.space *> strP)),
"/set voice " *> (SetUserFeature (ACF SCFVoice) <$> strP),
"/set files #" *> (SetGroupFeatureRole (AGFR SGFFiles) <$> displayNameP <*> _strP <*> optional memberRole),
"/set files @" *> (SetContactFeature (ACF SCFFiles) <$> displayNameP <*> optional (A.space *> strP)),
"/set files " *> (SetUserFeature (ACF SCFFiles) <$> strP),
"/set history #" *> (SetGroupFeature (AGFNR SGFHistory) <$> displayNameP <*> (A.space *> strP)),
"/set reactions #" *> (SetGroupFeature (AGFNR SGFReactions) <$> displayNameP <*> (A.space *> strP)),
"/set calls @" *> (SetContactFeature (ACF SCFCalls) <$> displayNameP <*> optional (A.space *> strP)),
"/set calls " *> (SetUserFeature (ACF SCFCalls) <$> strP),
"/set delete #" *> (SetGroupFeatureRole (AGFR SGFFullDelete) <$> displayNameP <*> _strP <*> optional memberRole),
"/set delete @" *> (SetContactFeature (ACF SCFFullDelete) <$> displayNameP <*> optional (A.space *> strP)),
"/set delete " *> (SetUserFeature (ACF SCFFullDelete) <$> strP),
"/set direct #" *> (SetGroupFeatureRole (AGFR SGFDirectMessages) <$> displayNameP <*> _strP <*> optional memberRole),
"/set disappear #" *> (SetGroupTimedMessages <$> displayNameP <*> (A.space *> timedTTLOnOffP)),
"/set disappear @" *> (SetContactTimedMessages <$> displayNameP <*> optional (A.space *> timedMessagesEnabledP)),
"/set disappear " *> (SetUserTimedMessages <$> (("yes" $> True) <|> ("no" $> False))),
"/set reports #" *> (SetGroupFeature (AGFNR SGFReports) <$> displayNameP <*> _strP),
"/set links #" *> (SetGroupFeatureRole (AGFR SGFSimplexLinks) <$> displayNameP <*> _strP <*> optional memberRole),
"/set admission review #" *> (SetGroupMemberAdmissionReview <$> displayNameP <*> (A.space *> memberCriteriaP)),
("/incognito" <* optional (A.space *> onOffP)) $> ChatHelp HSIncognito,
"/set device name " *> (SetLocalDeviceName <$> textP),
"/list remote hosts" $> ListRemoteHosts,
"/switch remote host " *> (SwitchRemoteHost <$> ("local" $> Nothing <|> (Just <$> A.decimal))),
"/start remote host " *> (StartRemoteHost <$> ("new" $> Nothing <|> (Just <$> ((,) <$> A.decimal <*> (" multicast=" *> onOffP <|> pure False)))) <*> optional (A.space *> rcCtrlAddressP) <*> optional (" port=" *> A.decimal)),
"/stop remote host " *> (StopRemoteHost <$> ("new" $> RHNew <|> RHId <$> A.decimal)),
"/delete remote host " *> (DeleteRemoteHost <$> A.decimal),
"/store remote file " *> (StoreRemoteFile <$> A.decimal <*> optional (" encrypt=" *> onOffP) <* A.space <*> filePath),
"/get remote file " *> (GetRemoteFile <$> A.decimal <* A.space <*> jsonP),
("/connect remote ctrl " <|> "/crc ") *> (ConnectRemoteCtrl <$> strP),
"/find remote ctrl" $> FindKnownRemoteCtrl,
"/confirm remote ctrl " *> (ConfirmRemoteCtrl <$> A.decimal),
"/verify remote ctrl " *> (VerifyRemoteCtrlSession <$> textP),
"/list remote ctrls" $> ListRemoteCtrls,
"/stop remote ctrl" $> StopRemoteCtrl,
"/delete remote ctrl " *> (DeleteRemoteCtrl <$> A.decimal),
"/_upload " *> (APIUploadStandaloneFile <$> A.decimal <* A.space <*> cryptoFileP),
"/_download info " *> (APIStandaloneFileInfo <$> strP),
"/_download " *> (APIDownloadStandaloneFile <$> A.decimal <* A.space <*> strP_ <*> cryptoFileP),
("/quit" <|> "/q" <|> "/exit") $> QuitChat,
("/version" <|> "/v") $> ShowVersion,
"/debug locks" $> DebugLocks,
"/debug event " *> (DebugEvent <$> jsonP),
"/get subs total " *> (GetAgentSubsTotal <$> A.decimal),
"/get servers summary " *> (GetAgentServersSummary <$> A.decimal),
"/reset servers stats" $> ResetAgentServersStats,
"/get subs" $> GetAgentSubs,
"/get subs details" $> GetAgentSubsDetails,
"/get workers" $> GetAgentWorkers,
"/get workers details" $> GetAgentWorkersDetails,
"/get queues" $> GetAgentQueuesInfo,
"//" *> (CustomChatCommand <$> A.takeByteString)
]
where
choice = A.choice . map (\p -> p <* A.takeWhile (== ' ') <* A.endOfInput)
connLinkP = do
(ACR m cReq) <- strP
sLink_ <- optional (A.space *> strP)
pure $ ACCL m (CCLink cReq sLink_)
connLinkP' = do
cReq <- strP
sLink_ <- optional (A.space *> strP)
pure $ CCLink cReq sLink_
connLinkP_ =
((Just <$> connLinkP) <|> A.takeTill (== ' ') $> Nothing)
incognitoP = (A.space *> ("incognito" <|> "i")) $> True <|> pure False
incognitoOnOffP = (A.space *> "incognito=" *> onOffP) <|> pure False
imagePrefix = (<>) <$> "data:" <*> ("image/png;base64," <|> "image/jpg;base64,")
imageP = safeDecodeUtf8 <$> ((<>) <$> imagePrefix <*> (B64.encode <$> base64P))
chatTypeP = A.char '@' $> CTDirect <|> A.char '#' $> CTGroup <|> A.char '*' $> CTLocal <|> A.char ':' $> CTContactConnection
chatPaginationP =
(CPLast <$ "count=" <*> A.decimal)
<|> (CPAfter <$ "after=" <*> A.decimal <* A.space <* "count=" <*> A.decimal)
<|> (CPBefore <$ "before=" <*> A.decimal <* A.space <* "count=" <*> A.decimal)
<|> (CPAround <$ "around=" <*> A.decimal <* A.space <* "count=" <*> A.decimal)
<|> (CPInitial <$ "initial=" <*> A.decimal)
paginationByTimeP =
(PTLast <$ "count=" <*> A.decimal)
<|> (PTAfter <$ "after=" <*> strP <* A.space <* "count=" <*> A.decimal)
<|> (PTBefore <$ "before=" <*> strP <* A.space <* "count=" <*> A.decimal)
mcTextP = MCText . safeDecodeUtf8 <$> A.takeByteString
msgContentP = "text " *> mcTextP <|> "json " *> jsonP
chatDeleteMode =
A.choice
[ " full" *> (CDMFull <$> notifyP),
" entity" *> (CDMEntity <$> notifyP),
" messages" $> CDMMessages,
CDMFull <$> notifyP -- backwards compatible
]
where
notifyP = " notify=" *> onOffP <|> pure True
sendMsgQuote msgDir = SendMessageQuote <$> displayNameP <* A.space <*> pure msgDir <*> quotedMsg <*> msgTextP
quotedMsg = safeDecodeUtf8 <$> (A.char '(' *> A.takeTill (== ')') <* A.char ')') <* optional A.space
reactionP = MREmoji <$> (mrEmojiChar <$?> (toEmoji <$> A.anyChar))
toEmoji = \case
'1' -> '👍'
'+' -> '👍'
'-' -> '👎'
')' -> '😀'
',' -> '😢'
'*' -> head "❤️"
'^' -> '🚀'
c -> c
composedMessagesTextP = do
text <- mcTextP
pure [composedMessage Nothing text]
updatedMessagesTextP = (`UpdatedMessage` []) <$> mcTextP
liveMessageP = " live=" *> onOffP <|> pure False
sendMessageTTLP = " ttl=" *> ((Just <$> A.decimal) <|> ("default" $> Nothing)) <|> pure Nothing
receiptSettings = do
enable <- onOffP
clearOverrides <- (" clear_overrides=" *> onOffP) <|> pure False
pure UserMsgReceiptSettings {enable, clearOverrides}
onOffP = ("on" $> True) <|> ("off" $> False)
profileNameDescr = (,) <$> displayNameP <*> shortDescrP
-- 'Help with bot':'link <ID>','Menu of commands':[...]
botCommandsP :: Parser [ChatBotCommand]
botCommandsP = commandP `A.sepBy'` A.char ','
where
commandP = do
label <- safeDecodeUtf8 <$> ((quoted <|> A.takeTill (== ':')) <* A.char ':')
when (T.null label) $ fail "empty command label"
A.peekChar' >>= \case
'{' -> A.char '{' *> (CBCMenu label <$> botCommandsP) <* A.char '}'
_ -> do
cmd <- safeDecodeUtf8 <$> (optional (A.char '/') *> (quoted <|> A.takeTill (A.inClass ":,}")))
(keyword, params) <- case T.words cmd of
[] -> fail "empty command"
k : ws -> pure (k, if null ws then Nothing else Just $ T.unwords ws)
pure CBCCommand {label, keyword, params}
quoted = A.char '\'' *> A.takeTill (== '\'') <* A.char '\''
newUserP = do
(cName, shortDescr) <- profileNameDescr
let profile = Just Profile {displayName = cName, fullName = "", shortDescr, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing}
pure NewUser {profile, pastTimestamp = False}
newBotUserP = do
files_ <- optional $ "files=" *> onOffP <* A.space
(cName, shortDescr) <- profileNameDescr
let preferences = case files_ of
Just True -> Nothing
_ -> Just (emptyChatPrefs :: Preferences) {files = Just FilesPreference {allow = FANo}}
profile = Just Profile {displayName = cName, fullName = "", shortDescr, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences}
pure NewUser {profile, pastTimestamp = False}
jsonP :: J.FromJSON a => Parser a
jsonP = J.eitherDecodeStrict' <$?> A.takeByteString
groupProfile = do
(gName, shortDescr) <- profileNameDescr
let groupPreferences =
Just
(emptyGroupPrefs :: GroupPreferences)
{ directMessages = Just DirectMessagesGroupPreference {enable = FEOn, role = Nothing},
history = Just HistoryGroupPreference {enable = FEOn}
}
pure GroupProfile {displayName = gName, fullName = "", shortDescr, description = Nothing, image = Nothing, groupPreferences, memberAdmission = Nothing}
memberCriteriaP = ("all" $> Just MCAll) <|> ("off" $> Nothing)
shortDescrP = do
descr <- A.takeWhile1 isSpace *> (T.dropWhileEnd isSpace <$> textP) <|> pure ""
pure $ if T.null descr then Nothing else Just $ T.take 160 descr
textP = safeDecodeUtf8 <$> A.takeByteString
pwdP = jsonP <|> (UserPwd . safeDecodeUtf8 <$> A.takeTill (== ' '))
verifyCodeP = safeDecodeUtf8 <$> A.takeWhile (\c -> isDigit c || c == ' ')
msgTextP = jsonP <|> textP
stringP = T.unpack . safeDecodeUtf8 <$> A.takeByteString
filePath = stringP
cryptoFileP = do
cfArgs <- optional $ CFArgs <$> (" key=" *> strP <* A.space) <*> (" nonce=" *> strP)
path <- filePath
pure $ CryptoFile path cfArgs
connMsgsP = L.fromList <$> connMsgP `A.sepBy1'` A.char ','
connMsgP = do
AgentConnId msgConnId <- strP <* A.char ':'
msgDbQueueId <- strP <* A.char ':'
ts <- strP
pure ConnMsgReq {msgConnId, msgDbQueueId, msgTs = Just ts}
memberRole =
A.choice
[ " owner" $> GROwner,
" admin" $> GRAdmin,
" moderator" $> GRModerator,
" member" $> GRMember,
" observer" $> GRObserver
]
chatNameP =
chatTypeP >>= \case
CTLocal -> pure $ ChatName CTLocal ""
ct -> ChatName ct <$> displayNameP
chatNameP' = ChatName <$> (chatTypeP <|> pure CTDirect) <*> displayNameP
chatRefP = do
chatTypeP >>= \case
CTGroup -> ChatRef CTGroup <$> A.decimal <*> optional gcScopeP
cType -> (\chatId -> ChatRef cType chatId Nothing) <$> A.decimal
sendRefP =
(A.char '@' $> SRDirect <*> A.decimal)
<|> (A.char '#' $> SRGroup <*> A.decimal <*> optional gcScopeP)
gcScopeP = "(_support" *> (GCSMemberSupport <$> optional (A.char ':' *> A.decimal)) <* A.char ')'
sendNameP =
(A.char '@' $> SNDirect <*> displayNameP)
<|> (A.char '#' $> SNGroup <*> displayNameP <*> gScopeNameP)
<|> ("/*" $> SNLocal)
gScopeNameP =
(supportPfx *> (Just . GSNMemberSupport <$> optional supportMember) <* A.char ')')
-- this branch fails on "(support" followed by incorrect syntax,
-- to avoid sending message to the whole group as `optional gScopeNameP` would do
<|> (optional supportPfx >>= mapM (\_ -> fail "bad chat scope"))
where
supportPfx = A.takeWhile isSpace *> "(support"
supportMember = safeDecodeUtf8 <$> (A.char ':' *> A.takeWhile isSpace *> (A.take . lengthTillLastParen =<< A.lookAhead displayNameP_))
lengthTillLastParen s = case B.unsnoc s of
Just (_, ')') -> B.length s - 1
_ -> B.length s
msgCountP = A.space *> A.decimal <|> pure 10
ciTTLDecimal = ("default" $> Nothing) <|> (Just <$> A.decimal)
ciTTL =
("day" $> 86400)
<|> ("week" $> (7 * 86400))
<|> ("month" $> (30 * 86400))
<|> ("year" $> (365 * 86400))
<|> ("none" $> 0)
timedTTLP =
("30s" $> 30)
<|> ("5min" $> 300)
<|> ("1h" $> 3600)
<|> ("8h" $> (8 * 3600))
<|> ("day" $> 86400)
<|> ("week" $> (7 * 86400))
<|> ("month" $> (30 * 86400))
<|> A.decimal
timedTTLOnOffP =
optional ("on" *> A.space) *> (Just <$> timedTTLP)
<|> ("off" $> Nothing)
timedMessagesEnabledP =
optional ("yes" *> A.space) *> (TMEEnableSetTTL <$> timedTTLP)
<|> ("yes" $> TMEEnableKeepTTL)
<|> ("no" $> TMEDisableKeepTTL)
operatorRolesP = do
operatorId' <- A.decimal
enabled' <- A.char ':' *> onOffP
smpRoles' <- (":smp=" *> srvRolesP) <|> pure allRoles
xftpRoles' <- (":xftp=" *> srvRolesP) <|> pure allRoles
pure ServerOperatorRoles {operatorId', enabled', smpRoles', xftpRoles'}
srvRolesP = srvRoles <$?> A.takeTill (\c -> c == ':' || c == ',')
where
srvRoles = \case
"off" -> Right $ ServerRoles False False
"proxy" -> Right ServerRoles {storage = False, proxy = True}
"storage" -> Right ServerRoles {storage = True, proxy = False}
"on" -> Right allRoles
_ -> Left "bad ServerRoles"
netCfgP = do
socksProxy <- "socks=" *> ("off" $> Nothing <|> "on" $> Just defaultSocksProxyWithAuth <|> Just <$> strP)
socksMode <- " socks-mode=" *> strP <|> pure SMAlways
hostMode <- " host-mode=" *> (textToHostMode . safeDecodeUtf8 <$?> A.takeTill (== ' ')) <|> pure (defaultHostMode socksProxy)
requiredHostMode <- (" required-host-mode" $> True) <|> pure False
smpProxyMode_ <- optional $ " smp-proxy=" *> strP
smpProxyFallback_ <- optional $ " smp-proxy-fallback=" *> strP
smpWebPortServers <- (" smp-web-port-servers=" *> strP) <|> (" smp-web-port" $> SWPAll) <|> pure SWPPreset
t_ <- optional $ " timeout=" *> A.decimal
logTLSErrors <- " log=" *> onOffP <|> pure False
let tcpTimeout_ = (1000000 *) <$> t_
pure $ SimpleNetCfg {socksProxy, socksMode, hostMode, requiredHostMode, smpProxyMode_, smpProxyFallback_, smpWebPortServers, tcpTimeout_, logTLSErrors}
#if !defined(dbPostgres)
dbKeyP = nonEmptyKey <$?> strP
nonEmptyKey k@(DBEncryptionKey s) = if BA.null s then Left "empty key" else Right k
dbEncryptionConfig currentKey newKey = DBEncryptionConfig {currentKey, newKey, keepKey = Just False}
#endif
-- TODO [short links] parser for address settings
autoAcceptP = ifM onOffP (businessAA <|> addressAA) (pure $ AddressSettings False Nothing Nothing)
where
addressAA = AddressSettings False <$> (Just . AutoAccept <$> (" incognito=" *> onOffP <|> pure False)) <*> autoReply
businessAA = " business" *> (AddressSettings True (Just $ AutoAccept False) <$> autoReply)
autoReply = optional (A.space *> msgContentP)
rcCtrlAddressP = RCCtrlAddress <$> ("addr=" *> strP) <*> (" iface=" *> (jsonP <|> text1P))
text1P = safeDecodeUtf8 <$> A.takeTill (== ' ')
char_ = optional . A.char
displayNameP :: Parser Text
displayNameP = safeDecodeUtf8 <$> displayNameP_
{-# INLINE displayNameP #-}
displayNameP_ :: Parser ByteString
displayNameP_ = quoted '\'' <|> takeNameTill (\c -> isSpace c || c == ',')
where
takeNameTill p =
A.peekChar' >>= \c ->
if refChar c then A.takeTill p else fail "invalid first character in display name"
quoted c = A.char c *> takeNameTill (== c) <* A.char c
refChar c = c > ' ' && c /= '#' && c /= '@' && c /= '\''
mkValidName :: String -> String
mkValidName = dropWhileEnd isSpace . take 50 . reverse . fst3 . foldl' addChar ("", '\NUL', 0 :: Int)
where
fst3 (x, _, _) = x
addChar (r, prev, punct) c = if validChar then (c' : r, c', punct') else (r, prev, punct)
where
c' = if isSpace c then ' ' else c
punct'
| isPunctuation c = punct + 1
| isSpace c = punct
| otherwise = 0
validChar
| c == '\'' = False
| prev == '\NUL' = c > ' ' && c /= '#' && c /= '@' && validFirstChar
| isSpace prev = validFirstChar || (punct == 0 && isPunctuation c)
| isPunctuation prev = validFirstChar || isSpace c || (punct < 3 && isPunctuation c)
| otherwise = validFirstChar || isSpace c || isMark c || isPunctuation c
validFirstChar = isLetter c || isNumber c || isSymbol c