2021-07-06 19:07:03 +01:00
{- # LANGUAGE DataKinds # -}
2021-07-04 18:42:24 +01:00
{- # LANGUAGE DuplicateRecordFields # -}
2021-06-25 18:18:24 +01:00
{- # LANGUAGE FlexibleContexts # -}
{- # LANGUAGE GADTs # -}
{- # LANGUAGE LambdaCase # -}
2022-10-14 13:06:33 +01:00
{- # LANGUAGE MultiWayIf # -}
2021-06-25 18:18:24 +01:00
{- # LANGUAGE NamedFieldPuns # -}
2023-09-10 21:11:35 +01:00
{- # LANGUAGE OverloadedRecordDot # -}
2021-06-25 18:18:24 +01:00
{- # LANGUAGE OverloadedStrings # -}
2021-07-04 18:42:24 +01:00
{- # LANGUAGE RankNTypes # -}
{- # LANGUAGE ScopedTypeVariables # -}
2023-01-21 22:56:33 +00:00
{- # LANGUAGE TemplateHaskell # -}
2021-07-25 20:23:52 +01:00
{- # LANGUAGE TupleSections # -}
2022-01-11 12:41:38 +00:00
{- # LANGUAGE TypeApplications # -}
2023-08-25 04:56:37 +08:00
{- # OPTIONS_GHC - fno - warn - ambiguous - fields # -}
2021-06-25 18:18:24 +01:00
module Simplex.Chat where
2021-09-04 07:32:56 +01:00
import Control.Applicative ( optional , ( <|> ) )
2023-08-25 04:56:37 +08:00
import Control.Concurrent.STM ( retry )
2023-07-09 23:24:38 +01:00
import qualified Control.Exception as E
2021-07-07 22:46:38 +01:00
import Control.Logger.Simple
2023-08-25 04:56:37 +08:00
import Control.Monad
2021-06-25 18:18:24 +01:00
import Control.Monad.Except
import Control.Monad.IO.Unlift
import Control.Monad.Reader
2021-07-12 19:00:03 +01:00
import Crypto.Random ( drgNew )
2022-03-13 19:34:03 +00:00
import qualified Data.Aeson as J
2021-07-12 19:00:03 +01:00
import Data.Attoparsec.ByteString.Char8 ( Parser )
2021-06-25 18:18:24 +01:00
import qualified Data.Attoparsec.ByteString.Char8 as A
2023-09-04 23:19:24 +01:00
import Data.Bifunctor ( bimap , first )
2022-03-10 15:45:40 +04:00
import qualified Data.ByteString.Base64 as B64
2021-06-25 18:18:24 +01:00
import Data.ByteString.Char8 ( ByteString )
2021-07-11 12:22:22 +01:00
import qualified Data.ByteString.Char8 as B
2023-05-30 11:18:27 +01:00
import Data.Char ( isSpace , toLower )
2023-04-05 21:59:12 +01:00
import Data.Constraint ( Dict ( .. ) )
2023-01-16 15:06:03 +04:00
import Data.Either ( fromRight , rights )
2022-05-04 13:31:00 +01:00
import Data.Fixed ( div' )
2021-06-25 18:18:24 +01:00
import Data.Functor ( ( $> ) )
2021-09-04 07:32:56 +01:00
import Data.Int ( Int64 )
2023-08-25 14:10:40 +01:00
import Data.List ( find , foldl' , isSuffixOf , partition , sortOn )
2022-03-10 15:45:40 +04:00
import Data.List.NonEmpty ( NonEmpty , nonEmpty )
import qualified Data.List.NonEmpty as L
2021-09-04 07:32:56 +01:00
import Data.Map.Strict ( Map )
import qualified Data.Map.Strict as M
2023-01-24 16:24:34 +04:00
import Data.Maybe ( catMaybes , fromMaybe , isJust , isNothing , listToMaybe , mapMaybe , maybeToList )
2021-07-04 18:42:24 +01:00
import Data.Text ( Text )
2021-06-25 18:18:24 +01:00
import qualified Data.Text as T
2023-03-14 11:42:44 +04:00
import Data.Text.Encoding ( encodeUtf8 )
2023-03-19 11:49:30 +00:00
import Data.Time ( NominalDiffTime , addUTCTime , defaultTimeLocale , formatTime )
2023-05-05 13:49:09 +04:00
import Data.Time.Clock ( UTCTime , diffUTCTime , getCurrentTime , nominalDay , nominalDiffTimeToSeconds )
2022-06-19 14:44:13 +01:00
import Data.Time.Clock.System ( SystemTime , systemToUTCTime )
2023-04-16 12:35:45 +02:00
import Data.Word ( Word32 )
2023-08-12 18:27:10 +01:00
import qualified Database.SQLite.Simple as SQL
2022-06-06 16:23:47 +01:00
import Simplex.Chat.Archive
2022-05-03 10:22:35 +01:00
import Simplex.Chat.Call
2021-06-25 18:18:24 +01:00
import Simplex.Chat.Controller
2022-02-22 14:05:45 +00:00
import Simplex.Chat.Markdown
2022-01-24 16:07:17 +00:00
import Simplex.Chat.Messages
2023-06-17 11:03:22 +01:00
import Simplex.Chat.Messages.CIContent
2022-08-02 15:36:12 +01:00
import Simplex.Chat.Options
2022-08-18 11:35:31 +04:00
import Simplex.Chat.ProfileGenerator ( generateRandomProfile )
2021-07-04 18:42:24 +01:00
import Simplex.Chat.Protocol
2021-07-05 20:05:07 +01:00
import Simplex.Chat.Store
2023-06-18 10:20:11 +01:00
import Simplex.Chat.Store.Connections
import Simplex.Chat.Store.Direct
import Simplex.Chat.Store.Files
import Simplex.Chat.Store.Groups
import Simplex.Chat.Store.Messages
import Simplex.Chat.Store.Profiles
import Simplex.Chat.Store.Shared
2021-07-04 18:42:24 +01:00
import Simplex.Chat.Types
2023-07-21 21:32:28 +01:00
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Util
2023-04-21 13:46:56 +04:00
import Simplex.FileTransfer.Client.Main ( maxFileSize )
2023-03-09 11:01:22 +00:00
import Simplex.FileTransfer.Client.Presets ( defaultXFTPServers )
2023-03-22 22:20:12 +04:00
import Simplex.FileTransfer.Description ( ValidFileDescription , gb , kb , mb )
2023-04-14 15:32:12 +04:00
import Simplex.FileTransfer.Protocol ( FileParty ( .. ) , FilePartyI )
2022-07-17 15:51:17 +01:00
import Simplex.Messaging.Agent as Agent
2023-08-25 14:10:40 +01:00
import Simplex.Messaging.Agent.Client ( AgentStatsKey ( .. ) , SubInfo ( .. ) , agentClientStore , temporaryAgentError )
2023-03-27 18:34:48 +01:00
import Simplex.Messaging.Agent.Env.SQLite ( AgentConfig ( .. ) , InitialAgentServers ( .. ) , createAgentStore , defaultAgentConfig )
2022-10-22 21:22:44 +01:00
import Simplex.Messaging.Agent.Lock
2021-06-25 18:18:24 +01:00
import Simplex.Messaging.Agent.Protocol
2023-08-12 18:27:10 +01:00
import Simplex.Messaging.Agent.Store.SQLite ( MigrationConfirmation ( .. ) , MigrationError , SQLiteStore ( dbNew ) , execSQL , upMigration , withConnection )
2023-08-16 21:21:12 +04:00
import Simplex.Messaging.Agent.Store.SQLite.DB ( SlowQueryStats ( .. ) )
2023-08-12 18:27:10 +01:00
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
2023-03-27 18:34:48 +01:00
import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations
2022-08-02 15:36:12 +01:00
import Simplex.Messaging.Client ( defaultNetworkConfig )
2021-07-27 08:08:05 +01:00
import qualified Simplex.Messaging.Crypto as C
2023-09-01 19:43:27 +01:00
import Simplex.Messaging.Crypto.File ( CryptoFile ( .. ) , CryptoFileArgs ( .. ) )
import qualified Simplex.Messaging.Crypto.File as CF
2022-01-11 12:41:38 +00:00
import Simplex.Messaging.Encoding
2022-01-11 08:50:44 +00:00
import Simplex.Messaging.Encoding.String
2022-11-09 10:48:24 +00:00
import Simplex.Messaging.Parsers ( base64P )
2023-09-10 22:40:15 +03:00
import Simplex.Messaging.Protocol ( AProtoServerWithAuth ( .. ) , AProtocolType ( .. ) , EntityId , ErrorType ( .. ) , MsgBody , MsgFlags ( .. ) , NtfServer , ProtoServerWithAuth , ProtocolTypeI , SProtocolType ( .. ) , SubscriptionMode ( .. ) , UserProtocol , userProtocol )
2021-09-04 07:32:56 +01:00
import qualified Simplex.Messaging.Protocol as SMP
2022-05-04 13:31:00 +01:00
import qualified Simplex.Messaging.TMap as TM
2022-07-25 14:04:27 +01:00
import Simplex.Messaging.Transport.Client ( defaultSocksProxy )
2022-07-17 15:51:17 +01:00
import Simplex.Messaging.Util
2023-09-05 20:15:50 +04:00
import Simplex.Messaging.Version
2021-07-27 08:08:05 +01:00
import System.Exit ( exitFailure , exitSuccess )
2023-03-22 18:48:38 +04:00
import System.FilePath ( combine , splitExtensions , takeFileName , ( </> ) )
2023-07-09 23:24:38 +01:00
import System.IO ( Handle , IOMode ( .. ) , SeekMode ( .. ) , hFlush , stdout )
2023-05-09 10:33:30 +02:00
import System.Random ( randomRIO )
2021-07-05 19:54:44 +01:00
import Text.Read ( readMaybe )
2022-02-25 16:29:36 +04:00
import UnliftIO.Async
2022-12-15 15:17:29 +04:00
import UnliftIO.Concurrent ( forkFinally , forkIO , mkWeakThreadId , threadDelay )
2022-04-15 13:16:34 +01:00
import UnliftIO.Directory
2023-07-09 23:24:38 +01:00
import UnliftIO.IO ( hClose , hSeek , hTell , openFile )
2021-06-25 18:18:24 +01:00
import UnliftIO.STM
2021-08-02 20:10:24 +01:00
defaultChatConfig :: ChatConfig
defaultChatConfig =
ChatConfig
{ agentConfig =
defaultAgentConfig
{ tcpPort = undefined , -- agent does not listen to TCP
2023-03-27 18:34:48 +01:00
tbqSize = 1024
2021-08-02 20:10:24 +01:00
} ,
2023-09-01 19:20:07 +04:00
chatVRange = supportedChatVRange ,
2023-03-27 18:34:48 +01:00
confirmMigrations = MCConsole ,
2022-07-23 14:49:04 +01:00
defaultServers =
2023-01-13 13:54:07 +04:00
DefaultAgentServers
2022-07-23 14:49:04 +01:00
{ smp = _defaultSMPServers ,
ntf = _defaultNtfServers ,
2023-03-09 11:01:22 +00:00
xftp = defaultXFTPServers ,
2022-08-02 15:36:12 +01:00
netCfg = defaultNetworkConfig
2022-07-23 14:49:04 +01:00
} ,
2023-01-17 14:07:47 +00:00
tbqSize = 1024 ,
2022-10-14 13:06:33 +01:00
fileChunkSize = 15780 , -- do not change
2023-03-13 10:30:32 +00:00
xftpDescrPartSize = 14000 ,
2022-10-14 13:06:33 +01:00
inlineFiles = defaultInlineFilesConfig ,
2023-06-16 13:43:06 +01:00
autoAcceptFileSize = 0 ,
2023-04-18 13:49:09 +04:00
xftpFileConfig = Just defaultXFTPFileConfig ,
2023-03-16 10:49:57 +04:00
tempDir = Nothing ,
2023-05-15 12:28:53 +02:00
showReactions = False ,
2023-07-13 23:48:25 +01:00
showReceipts = False ,
2023-01-16 09:13:46 +00:00
logLevel = CLLImportant ,
2022-02-25 16:29:36 +04:00
subscriptionEvents = False ,
2022-08-13 14:18:12 +01:00
hostEvents = False ,
2023-01-25 19:29:09 +04:00
testView = False ,
2023-05-26 14:03:26 +04:00
initialCleanupManagerDelay = 30 * 1000000 , -- 30 seconds
cleanupManagerInterval = 30 * 60 , -- 30 minutes
2023-06-12 13:45:39 +04:00
cleanupManagerStepDelay = 3 * 1000000 , -- 3 seconds
2023-05-26 14:03:26 +04:00
ciExpirationInterval = 30 * 60 * 1000000 -- 30 minutes
2021-07-07 22:46:38 +01:00
}
2022-11-14 08:04:11 +00:00
_defaultSMPServers :: NonEmpty SMPServerWithAuth
2022-05-11 16:52:08 +01:00
_defaultSMPServers =
2022-03-10 15:45:40 +04:00
L . fromList
2023-07-30 15:15:41 +01:00
[ " smp://h--vW7ZSkXPeOUpfxlFGgauQmXNFOzGoizak7Ult7cw=@smp15.simplex.im,oauu4bgijybyhczbnxtlggo6hiubahmeutaqineuyy23aojpih3dajad.onion " ,
" smp://hejn2gVIqNU6xjtGM3OwQeuk8ZEbDXVJXAlnSBJBWUA=@smp16.simplex.im,p3ktngodzi6qrf7w64mmde3syuzrv57y55hxabqcq3l5p6oi7yzze6qd.onion " ,
" smp://ZKe4uxF4Z_aLJJOEsC-Y6hSkXgQS5-oc442JQGkyP8M=@smp17.simplex.im,ogtwfxyi3h2h5weftjjpjmxclhb5ugufa5rcyrmg7j4xlch7qsr5nuqd.onion " ,
" smp://PtsqghzQKU83kYTlQ1VKg996dW4Cw4x_bvpKmiv8uns=@smp18.simplex.im,lyqpnwbs2zqfr45jqkncwpywpbtq7jrhxnib5qddtr6npjyezuwd3nqd.onion " ,
" smp://N_McQS3F9TGoh4ER0QstUf55kGnNSd-wXfNPZ7HukcM=@smp19.simplex.im,i53bbtoqhlc365k6kxzwdp5w3cdt433s7bwh3y32rcbml2vztiyyz5id.onion "
2022-03-10 15:45:40 +04:00
]
2022-05-11 16:52:08 +01:00
_defaultNtfServers :: [ NtfServer ]
2022-08-13 11:53:53 +01:00
_defaultNtfServers = [ " ntf://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@ntf2.simplex.im,ntg7jdjy2i3qbib3sykiho3enekwiaqg3icctliqhtqcg6jmoh6cxiad.onion " ]
2022-04-22 13:46:05 +01:00
2022-05-21 18:17:15 +04:00
maxImageSize :: Integer
2023-05-30 11:18:27 +01:00
maxImageSize = 261120 * 2 -- auto-receive on mobiles
imageExtensions :: [ String ]
imageExtensions = [ " .jpg " , " .jpeg " , " .png " , " .gif " ]
2022-05-21 18:17:15 +04:00
2023-05-15 12:28:53 +02:00
maxMsgReactions :: Int
maxMsgReactions = 3
2022-05-21 18:17:15 +04:00
fixedImagePreview :: ImageData
fixedImagePreview = ImageData " data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAKVJREFUeF7t1kENACEUQ0FQhnVQ9lfGO+xggITQdvbMzArPey+8fa3tAfwAEdABZQspQStgBssEcgAIkSAJkiAJljtEgiRIgmUCSZAESZAESZAEyx0iQRIkwTKBJEiCv5fgvTd1wDmn7QAP4AeIgA4oW0gJWgEzWCZwbQ7gAA7ggLKFOIADOKBMIAeAEAmSIAmSYLlDJEiCJFgmkARJkARJ8N8S/ADTZUewBvnTOQAAAABJRU5ErkJggg== "
2023-07-26 14:49:35 +04:00
smallGroupsRcptsMemLimit :: Int
smallGroupsRcptsMemLimit = 20
2021-07-07 22:46:38 +01:00
logCfg :: LogConfig
logCfg = LogConfig { lc_file = Nothing , lc_stderr = True }
2023-03-27 18:34:48 +01:00
createChatDatabase :: FilePath -> String -> MigrationConfirmation -> IO ( Either MigrationError ChatDatabase )
createChatDatabase filePrefix key confirmMigrations = runExceptT $ do
chatStore <- ExceptT $ createChatStore ( chatStoreFile filePrefix ) key confirmMigrations
agentStore <- ExceptT $ createAgentStore ( agentStoreFile filePrefix ) key confirmMigrations
2022-09-23 19:22:56 +01:00
pure ChatDatabase { chatStore , agentStore }
newChatController :: ChatDatabase -> Maybe User -> ChatConfig -> ChatOpts -> Maybe ( Notification -> IO () ) -> IO ChatController
2023-06-16 13:43:06 +01:00
newChatController ChatDatabase { chatStore , agentStore } user cfg @ ChatConfig { agentConfig = aCfg , defaultServers , inlineFiles , tempDir } ChatOpts { coreOptions = CoreChatOpts { smpServers , xftpServers , networkConfig , logLevel , logConnections , logServerHosts , logFile , tbqSize } , optFilesFolder , showReactions , allowInstantFiles , autoAcceptFileSize } sendToast = do
let inlineFiles' = if allowInstantFiles || autoAcceptFileSize > 0 then inlineFiles else inlineFiles { sendChunks = 0 , receiveInstant = False }
config = cfg { logLevel , showReactions , tbqSize , subscriptionEvents = logConnections , hostEvents = logServerHosts , defaultServers = configServers , inlineFiles = inlineFiles' , autoAcceptFileSize }
2022-04-10 17:13:06 +01:00
sendNotification = fromMaybe ( const $ pure () ) sendToast
2022-09-23 19:22:56 +01:00
firstTime = dbNew chatStore
2022-01-21 11:09:33 +00:00
activeTo <- newTVarIO ActiveNone
currentUser <- newTVarIO user
2023-03-27 18:34:48 +01:00
servers <- agentServers config
smpAgent <- getSMPAgentClient aCfg { tbqSize } servers agentStore
2022-02-06 16:18:01 +00:00
agentAsync <- newTVarIO Nothing
2023-03-27 18:34:48 +01:00
idsDrg <- newTVarIO =<< liftIO drgNew
2021-08-02 20:10:24 +01:00
inputQ <- newTBQueueIO tbqSize
2022-01-21 11:09:33 +00:00
outputQ <- newTBQueueIO tbqSize
2021-08-02 20:10:24 +01:00
notifyQ <- newTBQueueIO tbqSize
2023-09-10 22:40:15 +03:00
subscriptionMode <- newTVarIO SMSubscribe
2022-10-22 21:22:44 +01:00
chatLock <- newEmptyTMVarIO
2021-09-04 07:32:56 +01:00
sndFiles <- newTVarIO M . empty
rcvFiles <- newTVarIO M . empty
2022-05-04 13:31:00 +01:00
currentCalls <- atomically TM . empty
2022-12-26 22:24:34 +00:00
filesFolder <- newTVarIO optFilesFolder
2022-06-06 16:23:47 +01:00
chatStoreChanged <- newTVarIO False
2023-01-13 21:01:26 +04:00
expireCIThreads <- newTVarIO M . empty
expireCIFlags <- newTVarIO M . empty
2022-12-15 15:17:29 +04:00
cleanupManagerAsync <- newTVarIO Nothing
timedItemThreads <- atomically TM . empty
2022-12-19 11:16:50 +00:00
showLiveItems <- newTVarIO False
2023-03-13 10:30:32 +00:00
userXFTPFileConfig <- newTVarIO $ xftpFileConfig cfg
2023-03-16 10:49:57 +04:00
tempDirectory <- newTVarIO tempDir
2023-09-10 22:40:15 +03:00
pure ChatController { activeTo , firstTime , currentUser , smpAgent , agentAsync , chatStore , chatStoreChanged , idsDrg , inputQ , outputQ , notifyQ , subscriptionMode , chatLock , sndFiles , rcvFiles , currentCalls , config , sendNotification , filesFolder , expireCIThreads , expireCIFlags , cleanupManagerAsync , timedItemThreads , showLiveItems , userXFTPFileConfig , tempDirectory , logFilePath = logFile }
2022-03-10 15:45:40 +04:00
where
2023-01-13 13:54:07 +04:00
configServers :: DefaultAgentServers
2022-11-21 07:43:41 +00:00
configServers =
2023-08-25 04:56:37 +08:00
let smp' = fromMaybe ( defaultServers . smp ) ( nonEmpty smpServers )
xftp' = fromMaybe ( defaultServers . xftp ) ( nonEmpty xftpServers )
2023-03-16 14:12:19 +04:00
in defaultServers { smp = smp' , xftp = xftp' , netCfg = networkConfig }
2022-11-21 07:43:41 +00:00
agentServers :: ChatConfig -> IO InitialAgentServers
2023-03-09 11:01:22 +00:00
agentServers config @ ChatConfig { defaultServers = defServers @ DefaultAgentServers { ntf , netCfg } } = do
2023-01-13 13:54:07 +04:00
users <- withTransaction chatStore getUsers
2023-04-05 21:59:12 +01:00
smp' <- getUserServers users SPSMP
xftp' <- getUserServers users SPXFTP
2023-03-09 11:01:22 +00:00
pure InitialAgentServers { smp = smp' , xftp = xftp' , ntf , netCfg }
2022-11-21 07:43:41 +00:00
where
2023-04-05 21:59:12 +01:00
getUserServers :: forall p . ( ProtocolTypeI p , UserProtocol p ) => [ User ] -> SProtocolType p -> IO ( Map UserId ( NonEmpty ( ProtoServerWithAuth p ) ) )
getUserServers users protocol = case users of
[] -> pure $ M . fromList [ ( 1 , cfgServers protocol defServers ) ]
2023-03-09 11:01:22 +00:00
_ -> M . fromList <$> initialServers
where
initialServers :: IO [ ( UserId , NonEmpty ( ProtoServerWithAuth p ) ) ]
initialServers = mapM ( \ u -> ( aUserId u , ) <$> userServers u ) users
userServers :: User -> IO ( NonEmpty ( ProtoServerWithAuth p ) )
2023-04-05 21:59:12 +01:00
userServers user' = activeAgentServers config protocol <$> withTransaction chatStore ( ` getProtocolServers ` user' )
2023-03-09 11:01:22 +00:00
2023-04-05 21:59:12 +01:00
activeAgentServers :: UserProtocol p => ChatConfig -> SProtocolType p -> [ ServerCfg p ] -> NonEmpty ( ProtoServerWithAuth p )
activeAgentServers ChatConfig { defaultServers } p =
fromMaybe ( cfgServers p defaultServers )
2022-11-16 15:37:20 +00:00
. nonEmpty
. map ( \ ServerCfg { server } -> server )
. filter ( \ ServerCfg { enabled } -> enabled )
2023-04-05 21:59:12 +01:00
cfgServers :: UserProtocol p => SProtocolType p -> ( DefaultAgentServers -> NonEmpty ( ProtoServerWithAuth p ) )
2023-08-25 04:56:37 +08:00
cfgServers p s = case p of
SPSMP -> s . smp
SPXFTP -> s . xftp
2023-04-05 21:59:12 +01:00
2023-04-18 19:43:16 +04:00
startChatController :: forall m . ChatMonad' m => Bool -> Bool -> Bool -> m ( Async () )
startChatController subConns enableExpireCIs startXFTPWorkers = do
2022-06-06 16:23:47 +01:00
asks smpAgent >>= resumeAgentClient
2023-09-10 22:40:15 +03:00
unless subConns $
chatWriteVar subscriptionMode SMOnlyCreate
2023-05-29 15:18:22 +04:00
users <- fromRight [] <$> runExceptT ( withStoreCtx' ( Just " startChatController, getUsers " ) getUsers )
2023-05-29 11:19:03 +04:00
restoreCalls
2022-02-06 16:18:01 +00:00
s <- asks agentAsync
2023-01-13 21:01:26 +04:00
readTVarIO s >>= maybe ( start s users ) ( pure . fst )
2022-02-06 16:18:01 +00:00
where
2023-01-13 21:01:26 +04:00
start s users = do
2023-05-29 11:19:03 +04:00
a1 <- async $ race_ notificationSubscriber agentSubscriber
2023-05-11 16:00:01 +04:00
a2 <-
2023-05-29 11:19:03 +04:00
if subConns
2023-09-10 22:40:15 +03:00
then Just <$> async ( subscribeUsers False users )
2023-05-29 11:19:03 +04:00
else pure Nothing
2022-07-02 10:13:06 +01:00
atomically . writeTVar s $ Just ( a1 , a2 )
2023-04-20 16:52:55 +04:00
when startXFTPWorkers $ do
2023-05-29 11:19:03 +04:00
startXFTP
void $ forkIO $ startFilesToReceive users
startCleanupManager
when enableExpireCIs $ startExpireCIs users
2022-07-02 10:13:06 +01:00
pure a1
2023-03-22 18:48:38 +04:00
startXFTP = do
tmp <- readTVarIO =<< asks tempDirectory
runExceptT ( withAgent $ \ a -> xftpStartWorkers a tmp ) >>= \ case
Left e -> liftIO $ print $ " Error starting XFTP workers: " <> show e
Right _ -> pure ()
2022-12-15 15:17:29 +04:00
startCleanupManager = do
cleanupAsync <- asks cleanupManagerAsync
readTVarIO cleanupAsync >>= \ case
Nothing -> do
2023-01-14 19:21:10 +04:00
a <- Just <$> async ( void $ runExceptT cleanupManager )
2022-12-15 15:17:29 +04:00
atomically $ writeTVar cleanupAsync a
_ -> pure ()
2023-01-14 17:52:40 +04:00
startExpireCIs users =
forM_ users $ \ user -> do
2023-05-29 15:18:22 +04:00
ttl <- fromRight Nothing <$> runExceptT ( withStoreCtx' ( Just " startExpireCIs, getChatItemTTL " ) ( ` getChatItemTTL ` user ) )
2023-01-14 17:52:40 +04:00
forM_ ttl $ \ _ -> do
startExpireCIThread user
setExpireCIFlag user True
2022-07-05 15:15:15 +04:00
2023-09-10 22:40:15 +03:00
subscribeUsers :: forall m . ChatMonad' m => Bool -> [ User ] -> m ()
subscribeUsers onlyNeeded users = do
2023-01-14 15:45:13 +04:00
let ( us , us' ) = partition activeUser users
subscribe us
subscribe us'
where
subscribe :: [ User ] -> m ()
2023-09-10 22:40:15 +03:00
subscribe = mapM_ $ runExceptT . subscribeUserConnections onlyNeeded Agent . subscribeConnections
2023-01-14 15:45:13 +04:00
2023-04-20 16:52:55 +04:00
startFilesToReceive :: forall m . ChatMonad' m => [ User ] -> m ()
startFilesToReceive users = do
let ( us , us' ) = partition activeUser users
startReceive us
startReceive us'
where
startReceive :: [ User ] -> m ()
startReceive = mapM_ $ runExceptT . startReceiveUserFiles
2023-07-09 23:24:38 +01:00
startReceiveUserFiles :: ChatMonad m => User -> m ()
2023-04-20 16:52:55 +04:00
startReceiveUserFiles user = do
2023-05-29 15:18:22 +04:00
filesToReceive <- withStoreCtx' ( Just " startReceiveUserFiles, getRcvFilesToReceive " ) ( ` getRcvFilesToReceive ` user )
2023-04-20 16:52:55 +04:00
forM_ filesToReceive $ \ ft ->
2023-07-09 23:24:38 +01:00
flip catchChatError ( toView . CRChatError ( Just user ) ) $
2023-04-20 16:52:55 +04:00
toView =<< receiveFile' user ft Nothing Nothing
2023-03-22 15:58:01 +00:00
restoreCalls :: ChatMonad' m => m ()
2023-01-16 15:06:03 +04:00
restoreCalls = do
2023-05-29 15:18:22 +04:00
savedCalls <- fromRight [] <$> runExceptT ( withStoreCtx' ( Just " restoreCalls, getCalls " ) $ \ db -> getCalls db )
2022-07-05 15:15:15 +04:00
let callsMap = M . fromList $ map ( \ call @ Call { contactId } -> ( contactId , call ) ) savedCalls
calls <- asks currentCalls
atomically $ writeTVar calls callsMap
2022-01-24 16:07:17 +00:00
2023-01-10 20:52:59 +04:00
stopChatController :: forall m . MonadUnliftIO m => ChatController -> m ()
2023-01-13 21:01:26 +04:00
stopChatController ChatController { smpAgent , agentAsync = s , sndFiles , rcvFiles , expireCIFlags } = do
2022-04-25 16:30:21 +01:00
disconnectAgentClient smpAgent
2022-07-02 10:13:06 +01:00
readTVarIO s >>= mapM_ ( \ ( a1 , a2 ) -> uninterruptibleCancel a1 >> mapM_ uninterruptibleCancel a2 )
2023-01-10 20:52:59 +04:00
closeFiles sndFiles
closeFiles rcvFiles
2022-09-28 20:47:06 +04:00
atomically $ do
2023-01-13 21:01:26 +04:00
keys <- M . keys <$> readTVar expireCIFlags
forM_ keys $ \ k -> TM . insert k False expireCIFlags
2022-09-28 20:47:06 +04:00
writeTVar s Nothing
2023-01-10 20:52:59 +04:00
where
closeFiles :: TVar ( Map Int64 Handle ) -> m ()
closeFiles files = do
fs <- readTVarIO files
mapM_ hClose fs
atomically $ writeTVar files M . empty
2022-04-25 16:30:21 +01:00
2023-03-22 15:58:01 +00:00
execChatCommand :: ChatMonad' m => ByteString -> m ChatResponse
2023-01-16 17:25:06 +00:00
execChatCommand s = do
u <- readTVarIO =<< asks currentUser
case parseChatCommand s of
Left e -> pure $ chatCmdError u e
2023-08-01 20:54:51 +01:00
Right cmd -> execChatCommand_ u cmd
execChatCommand' :: ChatMonad' m => ChatCommand -> m ChatResponse
execChatCommand' cmd = asks currentUser >>= readTVarIO >>= ( ` execChatCommand_ ` cmd )
execChatCommand_ :: ChatMonad' m => Maybe User -> ChatCommand -> m ChatResponse
execChatCommand_ u cmd = either ( CRChatCmdError u ) id <$> runExceptT ( processChatCommand cmd )
2022-01-24 16:07:17 +00:00
2022-04-10 12:18:53 +01:00
parseChatCommand :: ByteString -> Either String ChatCommand
2022-11-30 08:25:42 +00:00
parseChatCommand = A . parseOnly chatCommandP . B . dropWhileEnd isSpace
2022-04-10 12:18:53 +01:00
2023-05-10 15:18:50 +04:00
toView :: ChatMonad' m => ChatResponse -> m ()
2022-01-24 16:07:17 +00:00
toView event = do
q <- asks outputQ
2022-01-26 21:20:08 +00:00
atomically $ writeTBQueue q ( Nothing , event )
2022-01-24 16:07:17 +00:00
2022-02-06 16:18:01 +00:00
processChatCommand :: forall m . ChatMonad m => ChatCommand -> m ChatResponse
processChatCommand = \ case
ShowActiveUser -> withUser' $ pure . CRActiveUser
2023-05-09 10:33:30 +02:00
CreateActiveUser NewUser { profile , sameServers , pastTimestamp } -> do
p @ Profile { displayName } <- liftIO $ maybe generateRandomProfile pure profile
2022-02-06 16:18:01 +00:00
u <- asks currentUser
2023-04-05 21:59:12 +01:00
( smp , smpServers ) <- chooseServers SPSMP
( xftp , xftpServers ) <- chooseServers SPXFTP
2023-01-13 13:54:07 +04:00
auId <-
withStore' getUsers >>= \ case
[] -> pure 1
2023-01-31 12:24:18 +00:00
users -> do
when ( any ( \ User { localDisplayName = n } -> n == displayName ) users ) $
throwChatError $ CEUserExists displayName
2023-04-05 21:59:12 +01:00
withAgent ( \ a -> createUser a smp xftp )
2023-05-09 10:33:30 +02:00
ts <- liftIO $ getCurrentTime >>= if pastTimestamp then coupleDaysAgo else pure
user <- withStore $ \ db -> createUserRecordAt db ( AgentUserId auId ) p True ts
2023-04-05 21:59:12 +01:00
storeServers user smpServers
storeServers user xftpServers
2023-01-13 21:01:36 +04:00
setActive ActiveNone
2022-02-06 16:18:01 +00:00
atomically . writeTVar u $ Just user
pure $ CRActiveUser user
2023-01-18 18:49:56 +04:00
where
2023-04-05 21:59:12 +01:00
chooseServers :: ( ProtocolTypeI p , UserProtocol p ) => SProtocolType p -> m ( NonEmpty ( ProtoServerWithAuth p ) , [ ServerCfg p ] )
chooseServers protocol
2023-01-18 18:49:56 +04:00
| sameServers =
asks currentUser >>= readTVarIO >>= \ case
Nothing -> throwChatError CENoActiveUser
Just user -> do
2023-06-06 14:17:14 +04:00
servers <- withStore' ( ` getProtocolServers ` user )
2023-01-18 18:49:56 +04:00
cfg <- asks config
2023-06-06 14:17:14 +04:00
pure ( activeAgentServers cfg protocol servers , servers )
2023-01-18 18:49:56 +04:00
| otherwise = do
2023-04-05 21:59:12 +01:00
defServers <- asks $ defaultServers . config
pure ( cfgServers protocol defServers , [] )
storeServers user servers =
unless ( null servers ) $
withStore $ \ db -> overwriteProtocolServers db user servers
2023-05-10 14:47:36 +02:00
coupleDaysAgo t = ( ` addUTCTime ` t ) . fromInteger . negate . ( + ( 2 * day ) ) <$> randomRIO ( 0 , day )
2023-05-09 10:33:30 +02:00
day = 86400
2023-05-29 15:18:22 +04:00
ListUsers -> CRUsersList <$> withStoreCtx' ( Just " ListUsers, getUsersInfo " ) getUsersInfo
2023-03-22 15:58:01 +00:00
APISetActiveUser userId' viewPwd_ -> withUser $ \ user -> do
user' <- privateGetUser userId'
validateUserPassword user user' viewPwd_
2023-05-29 15:18:22 +04:00
withStoreCtx' ( Just " APISetActiveUser, setActiveUser " ) $ \ db -> setActiveUser db userId'
2023-01-13 21:01:36 +04:00
setActive ActiveNone
2023-03-22 15:58:01 +00:00
let user'' = user' { activeUser = True }
asks currentUser >>= atomically . ( ` writeTVar ` Just user'' )
pure $ CRActiveUser user''
SetActiveUser uName viewPwd_ -> do
2023-08-12 18:27:10 +01:00
tryChatError ( withStore ( ` getUserIdByName ` uName ) ) >>= \ case
2023-03-22 15:58:01 +00:00
Left _ -> throwChatError CEUserUnknown
Right userId -> processChatCommand $ APISetActiveUser userId viewPwd_
2023-07-13 23:48:25 +01:00
SetAllContactReceipts onOff -> withUser $ \ _ -> withStore' ( ` updateAllContactReceipts ` onOff ) >> ok_
APISetUserContactReceipts userId' settings -> withUser $ \ user -> do
user' <- privateGetUser userId'
validateUserPassword user user' Nothing
withStore' $ \ db -> updateUserContactReceipts db user' settings
ok user
SetUserContactReceipts settings -> withUser $ \ User { userId } -> processChatCommand $ APISetUserContactReceipts userId settings
2023-07-26 14:49:35 +04:00
APISetUserGroupReceipts userId' settings -> withUser $ \ user -> do
user' <- privateGetUser userId'
validateUserPassword user user' Nothing
withStore' $ \ db -> updateUserGroupReceipts db user' settings
ok user
SetUserGroupReceipts settings -> withUser $ \ User { userId } -> processChatCommand $ APISetUserGroupReceipts userId settings
2023-03-29 17:39:04 +01:00
APIHideUser userId' ( UserPwd viewPwd ) -> withUser $ \ user -> do
2023-03-22 15:58:01 +00:00
user' <- privateGetUser userId'
case viewPwdHash user' of
Just _ -> throwChatError $ CEUserAlreadyHidden userId'
_ -> do
when ( T . null viewPwd ) $ throwChatError $ CEEmptyUserPassword userId'
users <- withStore' getUsers
unless ( length ( filter ( isNothing . viewPwdHash ) users ) > 1 ) $ throwChatError $ CECantHideLastUser userId'
viewPwdHash' <- hashPassword
2023-03-29 17:39:04 +01:00
setUserPrivacy user user' { viewPwdHash = viewPwdHash' , showNtfs = False }
2023-03-22 15:58:01 +00:00
where
hashPassword = do
salt <- drgRandomBytes 16
let hash = B64UrlByteString $ C . sha512Hash $ encodeUtf8 viewPwd <> salt
pure $ Just UserPwdHash { hash , salt = B64UrlByteString salt }
2023-03-29 19:28:06 +01:00
APIUnhideUser userId' viewPwd @ ( UserPwd pwd ) -> withUser $ \ user -> do
2023-03-22 15:58:01 +00:00
user' <- privateGetUser userId'
case viewPwdHash user' of
Nothing -> throwChatError $ CEUserNotHidden userId'
_ -> do
2023-03-29 19:28:06 +01:00
when ( T . null pwd ) $ throwChatError $ CEEmptyUserPassword userId'
validateUserPassword user user' $ Just viewPwd
2023-03-29 17:39:04 +01:00
setUserPrivacy user user' { viewPwdHash = Nothing , showNtfs = True }
2023-03-29 19:28:06 +01:00
APIMuteUser userId' -> setUserNotifications userId' False
APIUnmuteUser userId' -> setUserNotifications userId' True
2023-03-22 15:58:01 +00:00
HideUser viewPwd -> withUser $ \ User { userId } -> processChatCommand $ APIHideUser userId viewPwd
2023-03-29 19:28:06 +01:00
UnhideUser viewPwd -> withUser $ \ User { userId } -> processChatCommand $ APIUnhideUser userId viewPwd
MuteUser -> withUser $ \ User { userId } -> processChatCommand $ APIMuteUser userId
UnmuteUser -> withUser $ \ User { userId } -> processChatCommand $ APIUnmuteUser userId
2023-03-22 15:58:01 +00:00
APIDeleteUser userId' delSMPQueues viewPwd_ -> withUser $ \ user -> do
user' <- privateGetUser userId'
validateUserPassword user user' viewPwd_
checkDeleteChatUser user'
withChatLock " deleteUser " . procCmd $ deleteChatUser user' delSMPQueues
DeleteUser uName delSMPQueues viewPwd_ -> withUserName uName $ \ userId -> APIDeleteUser userId delSMPQueues viewPwd_
2023-04-18 19:43:16 +04:00
StartChat subConns enableExpireCIs startXFTPWorkers -> withUser' $ \ _ ->
2022-02-26 20:21:32 +00:00
asks agentAsync >>= readTVarIO >>= \ case
Just _ -> pure CRChatRunning
2023-04-18 19:43:16 +04:00
_ -> checkStoreNotChanged $ startChatController subConns enableExpireCIs startXFTPWorkers $> CRChatStarted
2022-06-06 16:23:47 +01:00
APIStopChat -> do
ask >>= stopChatController
pure CRChatStopped
2023-01-16 15:06:03 +04:00
APIActivateChat -> withUser $ \ _ -> do
restoreCalls
2023-04-13 20:46:07 +04:00
withAgent foregroundAgent
2023-09-10 22:40:15 +03:00
users <- withStoreCtx' ( Just " APIActivateChat, getUsers " ) getUsers
void . forkIO $ subscribeUsers True users
void . forkIO $ startFilesToReceive users
2023-01-13 21:01:26 +04:00
setAllExpireCIFlags True
2023-01-18 10:20:55 +00:00
ok_
2022-09-28 20:47:06 +04:00
APISuspendChat t -> do
2023-01-13 21:01:26 +04:00
setAllExpireCIFlags False
2022-09-28 20:47:06 +04:00
withAgent ( ` suspendAgent ` t )
2023-01-18 10:20:55 +00:00
ok_
2023-09-10 22:40:15 +03:00
ResubscribeAllConnections -> withStoreCtx' ( Just " ResubscribeAllConnections, getUsers " ) getUsers >>= subscribeUsers False >> ok_
2023-03-22 18:48:38 +04:00
-- has to be called before StartChat
SetTempFolder tf -> do
createDirectoryIfMissing True tf
asks tempDirectory >>= atomically . ( ` writeTVar ` Just tf )
ok_
2023-01-18 10:20:55 +00:00
SetFilesFolder ff -> do
createDirectoryIfMissing True ff
asks filesFolder >>= atomically . ( ` writeTVar ` Just ff )
ok_
2023-03-22 22:20:12 +04:00
APISetXFTPConfig cfg -> do
asks userXFTPFileConfig >>= atomically . ( ` writeTVar ` cfg )
ok_
2023-01-18 10:20:55 +00:00
APIExportArchive cfg -> checkChatStopped $ exportArchive cfg >> ok_
2023-03-19 11:49:30 +00:00
ExportArchive -> do
ts <- liftIO getCurrentTime
let filePath = " simplex-chat. " <> formatTime defaultTimeLocale " %FT%H%M%SZ " ts <> " .zip "
processChatCommand $ APIExportArchive $ ArchiveConfig filePath Nothing Nothing
2023-05-23 13:51:23 +04:00
APIImportArchive cfg -> checkChatStopped $ do
fileErrs <- importArchive cfg
setStoreChanged
pure $ CRArchiveImported fileErrs
2022-09-29 16:26:43 +01:00
APIDeleteStorage -> withStoreChanged deleteStorage
2022-09-05 14:54:39 +01:00
APIStorageEncryption cfg -> withStoreChanged $ sqlCipherExport cfg
2022-10-01 15:19:41 +01:00
ExecChatStoreSQL query -> CRSQLResult <$> withStore' ( ` execSQL ` query )
2022-09-17 16:06:27 +01:00
ExecAgentStoreSQL query -> CRSQLResult <$> withAgent ( ` execAgentStoreSQL ` query )
2023-08-12 18:27:10 +01:00
SlowSQLQueries -> do
ChatController { chatStore , smpAgent } <- ask
chatQueries <- slowQueries chatStore
agentQueries <- slowQueries $ agentClientStore smpAgent
pure CRSlowSQLQueries { chatQueries , agentQueries }
where
2023-08-16 21:21:12 +04:00
slowQueries st =
liftIO $
map ( uncurry SlowSQLQuery . first SQL . fromQuery )
. sortOn ( timeAvg . snd )
. M . assocs
<$> withConnection st ( readTVarIO . DB . slow )
2023-01-18 10:20:55 +00:00
APIGetChats userId withPCC -> withUserId userId $ \ user ->
2023-05-29 15:18:22 +04:00
CRApiChats user <$> withStoreCtx' ( Just " APIGetChats, getChatPreviews " ) ( \ db -> getChatPreviews db user withPCC )
2022-08-08 22:48:42 +04:00
APIGetChat ( ChatRef cType cId ) pagination search -> withUser $ \ user -> case cType of
2022-10-18 13:16:28 +04:00
-- TODO optimize queries calculating ChatStats, currently they're disabled
2022-10-25 12:50:26 +04:00
CTDirect -> do
2022-12-07 19:05:32 +04:00
directChat <- withStore ( \ db -> getDirectChat db user cId pagination search )
2023-01-04 21:06:28 +04:00
pure $ CRApiChat user ( AChat SCTDirect directChat )
CTGroup -> do
groupChat <- withStore ( \ db -> getGroupChat db user cId pagination search )
pure $ CRApiChat user ( AChat SCTGroup groupChat )
CTContactRequest -> pure $ chatCmdError ( Just user ) " not implemented "
CTContactConnection -> pure $ chatCmdError ( Just user ) " not supported "
2023-04-27 09:12:34 +02:00
APIGetChatItems pagination search -> withUser $ \ user -> do
chatItems <- withStore $ \ db -> getAllChatItems db user pagination search
pure $ CRChatItems user chatItems
2023-05-18 17:52:58 +02:00
APIGetChatItemInfo chatRef itemId -> withUser $ \ user -> do
2023-07-26 14:49:35 +04:00
( aci @ ( AChatItem cType dir _ ci ) , versions ) <- withStore $ \ db ->
2023-05-18 17:52:58 +02:00
( , ) <$> getAChatItem db user chatRef itemId <*> liftIO ( getChatItemVersions db itemId )
2023-05-19 14:52:51 +02:00
let itemVersions = if null versions then maybeToList $ mkItemVersion ci else versions
2023-07-26 14:49:35 +04:00
memberDeliveryStatuses <- case ( cType , dir ) of
( SCTGroup , SMDSnd ) -> do
withStore' ( ` getGroupSndStatuses ` itemId ) >>= \ case
[] -> pure Nothing
memStatuses -> pure $ Just $ map ( uncurry MemberDeliveryStatus ) memStatuses
_ -> pure Nothing
pure $ CRChatItemInfo user aci ChatItemInfo { itemVersions , memberDeliveryStatuses }
2023-05-11 16:00:01 +04:00
APISendMessage ( ChatRef cType chatId ) live itemTTL ( ComposedMessage file_ quotedItemId_ mc ) -> withUser $ \ user @ User { userId } -> withChatLock " sendMessage " $ case cType of
2022-01-30 10:49:13 +00:00
CTDirect -> do
2022-12-15 15:17:29 +04:00
ct @ Contact { contactId , localDisplayName = c , contactUsed } <- withStore $ \ db -> getContact db user chatId
2022-12-03 18:06:21 +00:00
assertDirectAllowed user MDSnd ct XMsgNew_
2022-10-25 12:50:26 +04:00
unless contactUsed $ withStore' $ \ db -> updateContactUsed db user ct
2022-12-13 14:52:34 +00:00
if isVoice mc && not ( featureAllowed SCFVoice forUser ct )
2023-01-04 21:06:28 +04:00
then pure $ chatCmdError ( Just user ) ( " feature not allowed " <> T . unpack ( chatFeatureNameText CFVoice ) )
2022-11-30 19:42:33 +04:00
else do
2023-03-13 10:30:32 +00:00
( fInv_ , ciFile_ , ft_ ) <- unzipMaybe3 <$> setupSndFileTransfer ct
2023-05-11 16:00:01 +04:00
timed_ <- sndContactCITimed live ct itemTTL
2023-03-13 10:30:32 +00:00
( msgContainer , quotedItem_ ) <- prepareMsg fInv_ timed_
2022-11-23 11:04:08 +00:00
( msg @ SndMessage { sharedMsgId } , _ ) <- sendDirectContactMessage ct ( XMsgNew msgContainer )
case ft_ of
Just ft @ FileTransferMeta { fileInline = Just IFMSent } ->
sendDirectFileInline ct ft sharedMsgId
_ -> pure ()
2022-12-16 07:51:04 +00:00
ci <- saveSndChatItem' user ( CDDirectSnd ct ) msg ( CISndMsgContent mc ) ciFile_ quotedItem_ timed_ live
2023-05-15 21:07:03 +04:00
forM_ ( timed_ >>= timedDeleteAt' ) $
2022-12-20 10:17:29 +00:00
startProximateTimedItemThread user ( ChatRef CTDirect contactId , chatItemId' ci )
2022-11-23 11:04:08 +00:00
setActive $ ActiveC c
2023-01-04 21:06:28 +04:00
pure $ CRNewChatItem user ( AChatItem SCTDirect SMDSnd ( DirectChat ct ) ci )
2022-04-10 13:30:58 +04:00
where
2022-10-14 13:06:33 +01:00
setupSndFileTransfer :: Contact -> m ( Maybe ( FileInvitation , CIFile 'MDSnd , FileTransferMeta ) )
2022-08-18 11:35:31 +04:00
setupSndFileTransfer ct = forM file_ $ \ file -> do
2023-03-13 10:30:32 +00:00
( fileSize , fileMode ) <- checkSndFile mc file 1
case fileMode of
SendFileSMP fileInline -> smpSndFileTransfer file fileSize fileInline
2023-03-14 11:42:44 +04:00
SendFileXFTP -> xftpSndFileTransfer user file fileSize 1 $ CGContact ct
2023-03-13 10:30:32 +00:00
where
2023-09-01 19:43:27 +01:00
smpSndFileTransfer :: CryptoFile -> Integer -> Maybe InlineFileMode -> m ( FileInvitation , CIFile 'MDSnd , FileTransferMeta )
smpSndFileTransfer ( CryptoFile _ ( Just _ ) ) _ _ = throwChatError $ CEFileInternal " locally encrypted files can't be sent via SMP " -- can only happen if XFTP is disabled
smpSndFileTransfer ( CryptoFile file Nothing ) fileSize fileInline = do
2023-09-10 22:40:15 +03:00
subMode <- chatReadVar subscriptionMode
2023-03-13 10:30:32 +00:00
( agentConnId_ , fileConnReq ) <-
if isJust fileInline
then pure ( Nothing , Nothing )
2023-09-10 22:40:15 +03:00
else bimap Just Just <$> withAgent ( \ a -> createConnection a ( aUserId user ) True SCMInvitation Nothing subMode )
2023-03-13 10:30:32 +00:00
let fileName = takeFileName file
fileInvitation = FileInvitation { fileName , fileSize , fileDigest = Nothing , fileConnReq , fileInline , fileDescr = Nothing }
chSize <- asks $ fileChunkSize . config
withStore' $ \ db -> do
2023-09-10 22:40:15 +03:00
ft @ FileTransferMeta { fileId } <- createSndDirectFileTransfer db userId ct file fileInvitation agentConnId_ chSize subMode
2023-03-13 10:30:32 +00:00
fileStatus <- case fileInline of
Just IFMSent -> createSndDirectInlineFT db ct ft $> CIFSSndTransfer 0 1
_ -> pure CIFSSndStored
2023-09-01 19:43:27 +01:00
let fileSource = Just $ CF . plain file
ciFile = CIFile { fileId , fileName , fileSize , fileSource , fileStatus , fileProtocol = FPSMP }
2023-03-13 10:30:32 +00:00
pure ( fileInvitation , ciFile , ft )
2022-12-15 15:17:29 +04:00
prepareMsg :: Maybe FileInvitation -> Maybe CITimed -> m ( MsgContainer , Maybe ( CIQuote 'CTDirect ) )
2023-03-13 10:30:32 +00:00
prepareMsg fInv_ timed_ = case quotedItemId_ of
Nothing -> pure ( MCSimple ( ExtMsgContent mc fInv_ ( ttl' <$> timed_ ) ( justTrue live ) ) , Nothing )
2022-04-10 13:30:58 +04:00
Just quotedItemId -> do
2022-12-03 15:40:31 +04:00
CChatItem _ qci @ ChatItem { meta = CIMeta { itemTs , itemSharedMsgId } , formattedText , file } <-
2022-12-20 12:58:15 +00:00
withStore $ \ db -> getDirectChatItem db user chatId quotedItemId
2022-12-03 15:40:31 +04:00
( origQmc , qd , sent ) <- quoteData qci
2022-04-10 13:30:58 +04:00
let msgRef = MsgRef { msgId = itemSharedMsgId , sentAt = itemTs , sent , memberId = Nothing }
2022-05-06 12:04:53 +04:00
qmc = quoteContent origQmc file
2022-04-10 13:30:58 +04:00
quotedItem = CIQuote { chatDir = qd , itemId = Just quotedItemId , sharedMsgId = itemSharedMsgId , sentAt = itemTs , content = qmc , formattedText }
2023-03-13 10:30:32 +00:00
pure ( MCQuote QuotedMsg { msgRef , content = qmc } ( ExtMsgContent mc fInv_ ( ttl' <$> timed_ ) ( justTrue live ) ) , Just quotedItem )
2022-03-16 13:20:47 +00:00
where
2022-12-03 15:40:31 +04:00
quoteData :: ChatItem c d -> m ( MsgContent , CIQDirection 'CTDirect , Bool )
2023-02-08 07:08:53 +00:00
quoteData ChatItem { meta = CIMeta { itemDeleted = Just _ } } = throwChatError CEInvalidQuote
2022-12-03 15:40:31 +04:00
quoteData ChatItem { content = CISndMsgContent qmc } = pure ( qmc , CIQDirectSnd , True )
quoteData ChatItem { content = CIRcvMsgContent qmc } = pure ( qmc , CIQDirectRcv , False )
2022-04-11 09:34:59 +01:00
quoteData _ = throwChatError CEInvalidQuote
2022-03-13 19:34:03 +00:00
CTGroup -> do
2023-06-24 12:36:07 +01:00
g @ ( Group gInfo _ ) <- withStore $ \ db -> getGroup db user chatId
2023-02-01 13:57:39 +00:00
assertUserGroupRole gInfo GRAuthor
2023-06-24 12:36:07 +01:00
send g
2022-04-10 13:30:58 +04:00
where
2023-06-24 12:36:07 +01:00
send g @ ( Group gInfo @ GroupInfo { groupId , membership , localDisplayName = gName } ms )
| isVoice mc && not ( groupFeatureAllowed SGFVoice gInfo ) = notAllowedError GFVoice
| not ( isVoice mc ) && isJust file_ && not ( groupFeatureAllowed SGFFiles gInfo ) = notAllowedError GFFiles
| otherwise = do
( fInv_ , ciFile_ , ft_ ) <- unzipMaybe3 <$> setupSndFileTransfer g ( length $ filter memberCurrent ms )
timed_ <- sndGroupCITimed live gInfo itemTTL
( msgContainer , quotedItem_ ) <- prepareMsg fInv_ timed_ membership
2023-07-26 14:49:35 +04:00
( msg @ SndMessage { sharedMsgId } , sentToMembers ) <- sendGroupMessage user gInfo ms ( XMsgNew msgContainer )
2023-06-24 12:36:07 +01:00
mapM_ ( sendGroupFileInline ms sharedMsgId ) ft_
ci <- saveSndChatItem' user ( CDGroupSnd gInfo ) msg ( CISndMsgContent mc ) ciFile_ quotedItem_ timed_ live
2023-07-26 14:49:35 +04:00
withStore' $ \ db ->
forM_ sentToMembers $ \ GroupMember { groupMemberId } ->
createGroupSndStatus db ( chatItemId' ci ) groupMemberId CISSndNew
2023-06-24 12:36:07 +01:00
forM_ ( timed_ >>= timedDeleteAt' ) $
startProximateTimedItemThread user ( ChatRef CTGroup groupId , chatItemId' ci )
setActive $ ActiveG gName
pure $ CRNewChatItem user ( AChatItem SCTGroup SMDSnd ( GroupChat gInfo ) ci )
notAllowedError f = pure $ chatCmdError ( Just user ) ( " feature not allowed " <> T . unpack ( groupFeatureNameText f ) )
2023-03-14 09:28:54 +00:00
setupSndFileTransfer :: Group -> Int -> m ( Maybe ( FileInvitation , CIFile 'MDSnd , FileTransferMeta ) )
setupSndFileTransfer g @ ( Group gInfo _ ) n = forM file_ $ \ file -> do
2023-03-13 10:30:32 +00:00
( fileSize , fileMode ) <- checkSndFile mc file $ fromIntegral n
case fileMode of
SendFileSMP fileInline -> smpSndFileTransfer file fileSize fileInline
2023-03-14 09:28:54 +00:00
SendFileXFTP -> xftpSndFileTransfer user file fileSize n $ CGGroup g
2023-03-13 10:30:32 +00:00
where
2023-09-01 19:43:27 +01:00
smpSndFileTransfer :: CryptoFile -> Integer -> Maybe InlineFileMode -> m ( FileInvitation , CIFile 'MDSnd , FileTransferMeta )
smpSndFileTransfer ( CryptoFile _ ( Just _ ) ) _ _ = throwChatError $ CEFileInternal " locally encrypted files can't be sent via SMP " -- can only happen if XFTP is disabled
smpSndFileTransfer ( CryptoFile file Nothing ) fileSize fileInline = do
2023-03-13 10:30:32 +00:00
let fileName = takeFileName file
fileInvitation = FileInvitation { fileName , fileSize , fileDigest = Nothing , fileConnReq = Nothing , fileInline , fileDescr = Nothing }
fileStatus = if fileInline == Just IFMSent then CIFSSndTransfer 0 1 else CIFSSndStored
chSize <- asks $ fileChunkSize . config
withStore' $ \ db -> do
ft @ FileTransferMeta { fileId } <- createSndGroupFileTransfer db userId gInfo file fileInvitation chSize
2023-09-01 19:43:27 +01:00
let fileSource = Just $ CF . plain file
ciFile = CIFile { fileId , fileName , fileSize , fileSource , fileStatus , fileProtocol = FPSMP }
2023-03-13 10:30:32 +00:00
pure ( fileInvitation , ciFile , ft )
2022-10-14 13:06:33 +01:00
sendGroupFileInline :: [ GroupMember ] -> SharedMsgId -> FileTransferMeta -> m ()
sendGroupFileInline ms sharedMsgId ft @ FileTransferMeta { fileInline } =
2023-01-12 16:31:27 +04:00
when ( fileInline == Just IFMSent ) . forM_ ms $ \ m ->
2023-07-09 23:24:38 +01:00
processMember m ` catchChatError ` ( toView . CRChatError ( Just user ) )
2023-01-12 16:31:27 +04:00
where
processMember m @ GroupMember { activeConn = Just conn @ Connection { connStatus } } =
2022-10-14 13:06:33 +01:00
when ( connStatus == ConnReady || connStatus == ConnSndReady ) $ do
void . withStore' $ \ db -> createSndGroupInlineFT db m conn ft
sendMemberFileInline m conn ft sharedMsgId
2023-01-12 16:31:27 +04:00
processMember _ = pure ()
2022-12-15 15:17:29 +04:00
prepareMsg :: Maybe FileInvitation -> Maybe CITimed -> GroupMember -> m ( MsgContainer , Maybe ( CIQuote 'CTGroup ) )
2023-03-13 10:30:32 +00:00
prepareMsg fInv_ timed_ membership = case quotedItemId_ of
Nothing -> pure ( MCSimple ( ExtMsgContent mc fInv_ ( ttl' <$> timed_ ) ( justTrue live ) ) , Nothing )
2022-04-10 13:30:58 +04:00
Just quotedItemId -> do
2022-12-03 15:40:31 +04:00
CChatItem _ qci @ ChatItem { meta = CIMeta { itemTs , itemSharedMsgId } , formattedText , file } <-
2022-06-18 20:06:13 +01:00
withStore $ \ db -> getGroupChatItem db user chatId quotedItemId
2022-12-03 15:40:31 +04:00
( origQmc , qd , sent , GroupMember { memberId } ) <- quoteData qci membership
2022-04-10 13:30:58 +04:00
let msgRef = MsgRef { msgId = itemSharedMsgId , sentAt = itemTs , sent , memberId = Just memberId }
2022-05-06 12:04:53 +04:00
qmc = quoteContent origQmc file
2022-04-10 13:30:58 +04:00
quotedItem = CIQuote { chatDir = qd , itemId = Just quotedItemId , sharedMsgId = itemSharedMsgId , sentAt = itemTs , content = qmc , formattedText }
2023-03-13 10:30:32 +00:00
pure ( MCQuote QuotedMsg { msgRef , content = qmc } ( ExtMsgContent mc fInv_ ( ttl' <$> timed_ ) ( justTrue live ) ) , Just quotedItem )
2022-03-16 13:20:47 +00:00
where
2022-12-03 15:40:31 +04:00
quoteData :: ChatItem c d -> GroupMember -> m ( MsgContent , CIQDirection 'CTGroup , Bool , GroupMember )
2023-02-08 07:08:53 +00:00
quoteData ChatItem { meta = CIMeta { itemDeleted = Just _ } } _ = throwChatError CEInvalidQuote
2022-12-03 15:40:31 +04:00
quoteData ChatItem { chatDir = CIGroupSnd , content = CISndMsgContent qmc } membership' = pure ( qmc , CIQGroupSnd , True , membership' )
quoteData ChatItem { chatDir = CIGroupRcv m , content = CIRcvMsgContent qmc } _ = pure ( qmc , CIQGroupRcv $ Just m , False , m )
quoteData _ _ = throwChatError CEInvalidQuote
2023-01-04 21:06:28 +04:00
CTContactRequest -> pure $ chatCmdError ( Just user ) " not supported "
CTContactConnection -> pure $ chatCmdError ( Just user ) " not supported "
2022-04-10 13:30:58 +04:00
where
2022-05-06 12:04:53 +04:00
quoteContent :: forall d . MsgContent -> Maybe ( CIFile d ) -> MsgContent
quoteContent qmc ciFile_
| replaceContent = MCText qTextOrFile
| otherwise = case qmc of
2022-10-15 14:48:07 +04:00
MCImage _ image -> MCImage qTextOrFile image
MCFile _ -> MCFile qTextOrFile
2022-11-18 20:02:24 +03:00
-- consider same for voice messages
-- MCVoice _ voice -> MCVoice qTextOrFile voice
2022-10-15 14:48:07 +04:00
_ -> qmc
2022-05-06 12:04:53 +04:00
where
-- if the message we're quoting with is one of the "large" MsgContents
-- we replace the quote's content with MCText
replaceContent = case mc of
MCText _ -> False
MCFile _ -> False
MCLink { } -> True
MCImage { } -> True
2023-03-09 11:01:22 +00:00
MCVideo { } -> True
2022-11-15 15:24:55 +04:00
MCVoice { } -> False
2022-05-06 12:04:53 +04:00
MCUnknown { } -> True
qText = msgContentText qmc
2023-08-25 04:56:37 +08:00
getFileName :: CIFile d -> String
getFileName CIFile { fileName } = fileName
qFileName = maybe qText ( T . pack . getFileName ) ciFile_
2022-05-06 12:04:53 +04:00
qTextOrFile = if T . null qText then qFileName else qText
2023-09-01 19:43:27 +01:00
xftpSndFileTransfer :: User -> CryptoFile -> Integer -> Int -> ContactOrGroup -> m ( FileInvitation , CIFile 'MDSnd , FileTransferMeta )
xftpSndFileTransfer user file @ ( CryptoFile filePath cfArgs ) fileSize n contactOrGroup = do
let fileName = takeFileName filePath
2023-03-14 09:28:54 +00:00
fileDescr = FileDescr { fileDescrText = " " , fileDescrPartNo = 0 , fileDescrComplete = False }
fInv = xftpFileInvitation fileName fileSize fileDescr
2023-09-01 19:43:27 +01:00
fsFilePath <- toFSFilePath filePath
let srcFile = CryptoFile fsFilePath cfArgs
aFileId <- withAgent $ \ a -> xftpSendFile a ( aUserId user ) srcFile ( roundedFDCount n )
2023-03-16 10:49:57 +04:00
-- TODO CRSndFileStart event for XFTP
2023-03-31 17:33:52 +04:00
chSize <- asks $ fileChunkSize . config
ft @ FileTransferMeta { fileId } <- withStore' $ \ db -> createSndFileTransferXFTP db user contactOrGroup file fInv ( AgentSndFileId aFileId ) chSize
2023-09-01 19:43:27 +01:00
let fileSource = Just $ CryptoFile filePath cfArgs
ciFile = CIFile { fileId , fileName , fileSize , fileSource , fileStatus = CIFSSndStored , fileProtocol = FPXFTP }
2023-03-14 09:28:54 +00:00
case contactOrGroup of
CGContact Contact { activeConn } -> withStore' $ \ db -> createSndFTDescrXFTP db user Nothing activeConn ft fileDescr
2023-07-09 23:24:38 +01:00
CGGroup ( Group _ ms ) -> forM_ ms $ \ m -> saveMemberFD m ` catchChatError ` ( toView . CRChatError ( Just user ) )
2023-03-14 09:28:54 +00:00
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 ) ) $
withStore' $ \ db -> createSndFTDescrXFTP db user ( Just m ) conn ft fileDescr
saveMemberFD _ = pure ()
2023-03-13 10:30:32 +00:00
pure ( fInv , ciFile , ft )
2022-10-14 13:06:33 +01:00
unzipMaybe3 :: Maybe ( a , b , c ) -> ( Maybe a , Maybe b , Maybe c )
unzipMaybe3 ( Just ( a , b , c ) ) = ( Just a , Just b , Just c )
unzipMaybe3 _ = ( Nothing , Nothing , Nothing )
2022-12-20 12:58:15 +00:00
APIUpdateChatItem ( ChatRef cType chatId ) itemId live mc -> withUser $ \ user -> withChatLock " updateChatItem " $ case cType of
2022-03-23 11:37:51 +00:00
CTDirect -> do
2022-12-20 12:58:15 +00:00
( ct @ Contact { contactId , localDisplayName = c } , cci ) <- withStore $ \ db -> ( , ) <$> getContact db user chatId <*> getDirectChatItem db user chatId itemId
2022-12-03 18:06:21 +00:00
assertDirectAllowed user MDSnd ct XMsgUpdate_
2022-12-20 10:17:29 +00:00
case cci of
2023-07-20 13:50:31 +04:00
CChatItem SMDSnd ci @ ChatItem { meta = CIMeta { itemSharedMsgId , itemTimed , itemLive , editable } , content = ciContent } -> do
case ( ciContent , itemSharedMsgId , editable ) of
( CISndMsgContent oldMC , Just itemSharedMId , True ) -> do
2023-05-11 16:00:01 +04:00
let changed = mc /= oldMC
if changed || fromMaybe False itemLive
2023-05-09 20:43:21 +04:00
then do
( SndMessage { msgId } , _ ) <- sendDirectContactMessage ct ( XMsgUpdate itemSharedMId mc ( ttl' <$> itemTimed ) ( justTrue . ( live && ) =<< itemLive ) )
ci' <- withStore' $ \ db -> do
currentTs <- liftIO getCurrentTime
2023-05-11 16:00:01 +04:00
when changed $
addInitialAndNewCIVersions db itemId ( chatItemTs' ci , oldMC ) ( currentTs , mc )
2023-05-09 20:43:21 +04:00
updateDirectChatItem' db user contactId ci ( CISndMsgContent mc ) live $ Just msgId
startUpdatedTimedItemThread user ( ChatRef CTDirect contactId ) ci ci'
setActive $ ActiveC c
pure $ CRChatItemUpdated user ( AChatItem SCTDirect SMDSnd ( DirectChat ct ) ci' )
2023-05-11 16:00:01 +04:00
else pure $ CRChatItemNotChanged user ( AChatItem SCTDirect SMDSnd ( DirectChat ct ) ci )
2022-03-28 20:35:57 +04:00
_ -> throwChatError CEInvalidChatItemUpdate
CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate
2022-03-23 11:37:51 +00:00
CTGroup -> do
2023-02-01 13:57:39 +00:00
Group gInfo @ GroupInfo { groupId , localDisplayName = gName } ms <- withStore $ \ db -> getGroup db user chatId
assertUserGroupRole gInfo GRAuthor
2022-12-20 10:17:29 +00:00
cci <- withStore $ \ db -> getGroupChatItem db user chatId itemId
case cci of
2023-07-20 13:50:31 +04:00
CChatItem SMDSnd ci @ ChatItem { meta = CIMeta { itemSharedMsgId , itemTimed , itemLive , editable } , content = ciContent } -> do
case ( ciContent , itemSharedMsgId , editable ) of
( CISndMsgContent oldMC , Just itemSharedMId , True ) -> do
2023-05-11 16:00:01 +04:00
let changed = mc /= oldMC
if changed || fromMaybe False itemLive
2023-05-09 20:43:21 +04:00
then do
2023-07-26 14:49:35 +04:00
( SndMessage { msgId } , _ ) <- sendGroupMessage user gInfo ms ( XMsgUpdate itemSharedMId mc ( ttl' <$> itemTimed ) ( justTrue . ( live && ) =<< itemLive ) )
2023-05-09 20:43:21 +04:00
ci' <- withStore' $ \ db -> do
currentTs <- liftIO getCurrentTime
2023-05-11 16:00:01 +04:00
when changed $
addInitialAndNewCIVersions db itemId ( chatItemTs' ci , oldMC ) ( currentTs , mc )
2023-05-09 20:43:21 +04:00
updateGroupChatItem db user groupId ci ( CISndMsgContent mc ) live $ Just msgId
startUpdatedTimedItemThread user ( ChatRef CTGroup groupId ) ci ci'
setActive $ ActiveG gName
pure $ CRChatItemUpdated user ( AChatItem SCTGroup SMDSnd ( GroupChat gInfo ) ci' )
2023-05-11 16:00:01 +04:00
else pure $ CRChatItemNotChanged user ( AChatItem SCTGroup SMDSnd ( GroupChat gInfo ) ci )
2022-03-28 20:35:57 +04:00
_ -> throwChatError CEInvalidChatItemUpdate
CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate
2023-01-04 21:06:28 +04:00
CTContactRequest -> pure $ chatCmdError ( Just user ) " not supported "
CTContactConnection -> pure $ chatCmdError ( Just user ) " not supported "
2022-12-20 12:58:15 +00:00
APIDeleteChatItem ( ChatRef cType chatId ) itemId mode -> withUser $ \ user -> withChatLock " deleteChatItem " $ case cType of
2022-03-28 20:35:57 +04:00
CTDirect -> do
2023-07-20 13:50:31 +04:00
( ct @ Contact { localDisplayName = c } , ci @ ( CChatItem msgDir ChatItem { meta = CIMeta { itemSharedMsgId , editable } } ) ) <- withStore $ \ db -> ( , ) <$> getContact db user chatId <*> getDirectChatItem db user chatId itemId
case ( mode , msgDir , itemSharedMsgId , editable ) of
( CIDMInternal , _ , _ , _ ) -> deleteDirectCI user ct ci True False
( CIDMBroadcast , SMDSnd , Just itemSharedMId , True ) -> do
2022-12-15 15:17:29 +04:00
assertDirectAllowed user MDSnd ct XMsgDel_
2023-02-08 07:08:53 +00:00
( SndMessage { msgId } , _ ) <- sendDirectContactMessage ct ( XMsgDel itemSharedMId Nothing )
2022-03-28 20:35:57 +04:00
setActive $ ActiveC c
2022-12-13 14:52:34 +00:00
if featureAllowed SCFFullDelete forUser ct
2022-12-15 15:17:29 +04:00
then deleteDirectCI user ct ci True False
2023-05-19 14:52:51 +02:00
else markDirectCIDeleted user ct ci msgId True =<< liftIO getCurrentTime
2023-07-20 13:50:31 +04:00
( CIDMBroadcast , _ , _ , _ ) -> throwChatError CEInvalidChatItemDelete
2022-03-28 20:35:57 +04:00
CTGroup -> do
2023-02-08 07:08:53 +00:00
Group gInfo ms <- withStore $ \ db -> getGroup db user chatId
2023-07-20 13:50:31 +04:00
ci @ ( CChatItem msgDir ChatItem { meta = CIMeta { itemSharedMsgId , editable } } ) <- withStore $ \ db -> getGroupChatItem db user chatId itemId
case ( mode , msgDir , itemSharedMsgId , editable ) of
( CIDMInternal , _ , _ , _ ) -> deleteGroupCI user gInfo ci True False Nothing =<< liftIO getCurrentTime
( CIDMBroadcast , SMDSnd , Just itemSharedMId , True ) -> do
2023-02-08 07:08:53 +00:00
assertUserGroupRole gInfo GRObserver -- can still delete messages sent earlier
2023-07-26 14:49:35 +04:00
( SndMessage { msgId } , _ ) <- sendGroupMessage user gInfo ms $ XMsgDel itemSharedMId Nothing
2023-02-08 22:29:36 +04:00
delGroupChatItem user gInfo ci msgId Nothing
2023-07-20 13:50:31 +04:00
( CIDMBroadcast , _ , _ , _ ) -> throwChatError CEInvalidChatItemDelete
2023-01-04 21:06:28 +04:00
CTContactRequest -> pure $ chatCmdError ( Just user ) " not supported "
CTContactConnection -> pure $ chatCmdError ( Just user ) " not supported "
2023-02-08 07:08:53 +00:00
APIDeleteMemberChatItem gId mId itemId -> withUser $ \ user -> withChatLock " deleteChatItem " $ do
2023-02-08 22:29:36 +04:00
Group gInfo @ GroupInfo { membership } ms <- withStore $ \ db -> getGroup db user gId
2023-02-08 07:08:53 +00:00
ci @ ( CChatItem _ ChatItem { chatDir , meta = CIMeta { itemSharedMsgId } } ) <- withStore $ \ db -> getGroupChatItem db user gId itemId
case ( chatDir , itemSharedMsgId ) of
( CIGroupRcv GroupMember { groupMemberId , memberRole , memberId } , Just itemSharedMId ) -> do
when ( groupMemberId /= mId ) $ throwChatError CEInvalidChatItemDelete
assertUserGroupRole gInfo $ max GRAdmin memberRole
2023-07-26 14:49:35 +04:00
( SndMessage { msgId } , _ ) <- sendGroupMessage user gInfo ms $ XMsgDel itemSharedMId $ Just memberId
2023-02-08 22:29:36 +04:00
delGroupChatItem user gInfo ci msgId ( Just membership )
2023-02-08 07:08:53 +00:00
( _ , _ ) -> throwChatError CEInvalidChatItemDelete
2023-05-17 01:22:00 +02:00
APIChatItemReaction ( ChatRef cType chatId ) itemId add reaction -> withUser $ \ user -> withChatLock " chatItemReaction " $ case cType of
2023-05-15 12:28:53 +02:00
CTDirect ->
withStore ( \ db -> ( , ) <$> getContact db user chatId <*> getDirectChatItem db user chatId itemId ) >>= \ case
( ct , CChatItem md ci @ ChatItem { meta = CIMeta { itemSharedMsgId = Just itemSharedMId } } ) -> do
unless ( featureAllowed SCFReactions forUser ct ) $
throwChatError $ CECommandError $ " feature not allowed " <> T . unpack ( chatFeatureNameText CFReactions )
unless ( ciReactionAllowed ci ) $
throwChatError $ CECommandError " reaction not allowed - chat item has no content "
rs <- withStore' $ \ db -> getDirectReactions db ct itemSharedMId True
checkReactionAllowed rs
( SndMessage { msgId } , _ ) <- sendDirectContactMessage ct $ XMsgReact itemSharedMId Nothing reaction add
createdAt <- liftIO getCurrentTime
reactions <- withStore' $ \ 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
2023-05-17 01:22:00 +02:00
pure $ CRChatItemReaction user add r
2023-05-15 12:28:53 +02:00
_ -> throwChatError $ CECommandError " reaction not possible - no shared item ID "
CTGroup ->
withStore ( \ db -> ( , ) <$> getGroup db user chatId <*> getGroupChatItem db user chatId itemId ) >>= \ case
2023-05-15 13:43:22 +02:00
( Group g @ GroupInfo { membership } ms , CChatItem md ci @ ChatItem { meta = CIMeta { itemSharedMsgId = Just itemSharedMId } } ) -> do
2023-05-15 12:28:53 +02:00
unless ( groupFeatureAllowed SGFReactions g ) $
throwChatError $ CECommandError $ " feature not allowed " <> T . unpack ( chatFeatureNameText CFReactions )
unless ( ciReactionAllowed ci ) $
throwChatError $ CECommandError " reaction not allowed - chat item has no content "
2023-05-15 13:43:22 +02:00
let GroupMember { memberId = itemMemberId } = chatItemMember g ci
2023-05-15 12:28:53 +02:00
rs <- withStore' $ \ db -> getGroupReactions db g membership itemMemberId itemSharedMId True
checkReactionAllowed rs
2023-07-26 14:49:35 +04:00
( SndMessage { msgId } , _ ) <- sendGroupMessage user g ms ( XMsgReact itemSharedMId ( Just itemMemberId ) reaction add )
2023-05-15 12:28:53 +02:00
createdAt <- liftIO getCurrentTime
reactions <- withStore' $ \ 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 ) $ CIReaction CIGroupSnd ci' createdAt reaction
2023-05-17 01:22:00 +02:00
pure $ CRChatItemReaction user add r
2023-05-15 12:28:53 +02:00
_ -> throwChatError $ CECommandError " reaction not possible - no shared item ID "
CTContactRequest -> pure $ chatCmdError ( Just user ) " not supported "
CTContactConnection -> pure $ chatCmdError ( Just user ) " not supported "
where
checkReactionAllowed rs = do
when ( ( reaction ` elem ` rs ) == add ) $
throwChatError $ CECommandError $ " reaction already " <> if add then " added " else " removed "
when ( add && length rs >= maxMsgReactions ) $
throwChatError $ CECommandError " too many reactions "
2023-01-17 13:08:51 +04:00
APIChatRead ( ChatRef cType chatId ) fromToIds -> withUser $ \ _ -> case cType of
2022-12-15 15:17:29 +04:00
CTDirect -> do
2023-01-17 13:08:51 +04:00
user <- withStore $ \ db -> getUserByContactId db chatId
2022-12-15 15:17:29 +04:00
timedItems <- withStore' $ \ db -> getDirectUnreadTimedItems db user chatId fromToIds
ts <- liftIO getCurrentTime
forM_ timedItems $ \ ( itemId , ttl ) -> do
2022-12-17 14:49:03 +04:00
let deleteAt = addUTCTime ( realToFrac ttl ) ts
2022-12-15 15:17:29 +04:00
withStore' $ \ db -> setDirectChatItemDeleteAt db user chatId itemId deleteAt
2022-12-20 10:17:29 +00:00
startProximateTimedItemThread user ( ChatRef CTDirect chatId , itemId ) deleteAt
2022-12-20 12:58:15 +00:00
withStore' $ \ db -> updateDirectChatItemsRead db user chatId fromToIds
2023-01-18 10:20:55 +00:00
ok user
2022-12-15 15:17:29 +04:00
CTGroup -> do
2023-01-17 13:08:51 +04:00
user @ User { userId } <- withStore $ \ db -> getUserByGroupId db chatId
2022-12-15 15:17:29 +04:00
timedItems <- withStore' $ \ db -> getGroupUnreadTimedItems db user chatId fromToIds
ts <- liftIO getCurrentTime
forM_ timedItems $ \ ( itemId , ttl ) -> do
2022-12-17 14:49:03 +04:00
let deleteAt = addUTCTime ( realToFrac ttl ) ts
2022-12-15 15:17:29 +04:00
withStore' $ \ db -> setGroupChatItemDeleteAt db user chatId itemId deleteAt
2022-12-20 10:17:29 +00:00
startProximateTimedItemThread user ( ChatRef CTGroup chatId , itemId ) deleteAt
2022-12-15 15:17:29 +04:00
withStore' $ \ db -> updateGroupChatItemsRead db userId chatId fromToIds
2023-01-18 10:20:55 +00:00
ok user
2023-01-17 13:08:51 +04:00
CTContactRequest -> pure $ chatCmdError Nothing " not supported "
CTContactConnection -> pure $ chatCmdError Nothing " not supported "
2022-11-15 10:31:44 +04:00
APIChatUnread ( ChatRef cType chatId ) unreadChat -> withUser $ \ user -> case cType of
2022-10-19 21:38:44 +03:00
CTDirect -> do
2022-10-25 12:50:26 +04:00
withStore $ \ db -> do
2022-11-15 10:31:44 +04:00
ct <- getContact db user chatId
2022-10-25 12:50:26 +04:00
liftIO $ updateContactUnreadChat db user ct unreadChat
2023-01-18 10:20:55 +00:00
ok user
2022-10-19 21:38:44 +03:00
CTGroup -> do
2022-10-25 12:50:26 +04:00
withStore $ \ db -> do
Group { groupInfo } <- getGroup db user chatId
liftIO $ updateGroupUnreadChat db user groupInfo unreadChat
2023-01-18 10:20:55 +00:00
ok user
2023-01-04 21:06:28 +04:00
_ -> pure $ chatCmdError ( Just user ) " not supported "
2022-05-05 10:37:53 +01:00
APIDeleteChat ( ChatRef cType chatId ) -> withUser $ \ user @ User { userId } -> case cType of
2022-01-31 21:53:53 +04:00
CTDirect -> do
2022-11-15 10:31:44 +04:00
ct @ Contact { localDisplayName } <- withStore $ \ db -> getContact db user chatId
2022-10-20 19:27:00 +04:00
filesInfo <- withStore' $ \ db -> getContactFileInfo db user ct
2023-01-24 16:24:34 +04:00
contactConnIds <- map aConnId <$> withStore ( \ db -> getContactConnections db userId ct )
2022-10-22 21:22:44 +01:00
withChatLock " deleteChat direct " . procCmd $ do
2023-01-24 16:24:34 +04:00
fileAgentConnIds <- concat <$> forM filesInfo ( deleteFile user )
deleteAgentConnectionsAsync user $ fileAgentConnIds <> contactConnIds
2022-10-20 19:27:00 +04:00
-- functions below are called in separate transactions to prevent crashes on android
-- (possibly, race condition on integrity check?)
withStore' $ \ db -> deleteContactConnectionsAndFiles db userId ct
2022-10-27 14:25:48 +04:00
withStore' $ \ db -> deleteContact db user ct
2022-10-20 19:27:00 +04:00
unsetActive $ ActiveC localDisplayName
2023-01-04 21:06:28 +04:00
pure $ CRContactDeleted user ct
2022-10-22 21:22:44 +01:00
CTContactConnection -> withChatLock " deleteChat contactConnection " . procCmd $ do
2023-01-24 16:24:34 +04:00
conn @ PendingContactConnection { pccAgentConnId = AgentConnId acId } <- withStore $ \ db -> getPendingContactConnection db userId chatId
deleteAgentConnectionAsync user acId
2022-06-18 20:06:13 +01:00
withStore' $ \ db -> deletePendingContactConnection db userId chatId
2023-01-04 21:06:28 +04:00
pure $ CRContactConnectionDeleted user conn
2022-07-12 19:20:56 +04:00
CTGroup -> do
2022-08-04 11:12:50 +01:00
Group gInfo @ GroupInfo { membership } members <- withStore $ \ db -> getGroup db user chatId
2023-08-25 04:56:37 +08:00
let isOwner = membership . memberRole == GROwner
2023-02-01 13:57:39 +00:00
canDelete = isOwner || not ( memberCurrent membership )
2023-02-08 07:08:53 +00:00
unless canDelete $ throwChatError $ CEGroupUserRole gInfo GROwner
2022-10-04 01:33:36 +04:00
filesInfo <- withStore' $ \ db -> getGroupFileInfo db user gInfo
2022-10-22 21:22:44 +01:00
withChatLock " deleteChat group " . procCmd $ do
2023-01-24 16:24:34 +04:00
deleteFilesAndConns user filesInfo
2023-02-01 13:57:39 +00:00
when ( memberActive membership && isOwner ) . void $ sendGroupMessage user gInfo members XGrpDel
deleteGroupLinkIfExists user gInfo
2023-01-24 16:24:34 +04:00
deleteMembersConnections user members
2022-09-14 19:45:21 +04:00
-- functions below are called in separate transactions to prevent crashes on android
2022-08-02 14:10:03 +04:00
-- (possibly, race condition on integrity check?)
2022-08-04 11:12:50 +01:00
withStore' $ \ db -> deleteGroupConnectionsAndFiles db user gInfo members
2022-10-20 19:27:00 +04:00
withStore' $ \ db -> deleteGroupItemsAndMembers db user gInfo members
2022-08-04 11:12:50 +01:00
withStore' $ \ db -> deleteGroup db user gInfo
2022-12-06 17:12:39 +04:00
let contactIds = mapMaybe memberContactId members
2023-01-24 17:58:08 +04:00
deleteAgentConnectionsAsync user . concat =<< mapM deleteUnusedContact contactIds
2023-01-04 21:06:28 +04:00
pure $ CRGroupDeletedUser user gInfo
2022-12-06 17:12:39 +04:00
where
2023-01-24 17:58:08 +04:00
deleteUnusedContact :: ContactId -> m [ ConnId ]
deleteUnusedContact contactId =
( withStore ( \ db -> getContact db user contactId ) >>= delete )
2023-07-09 23:24:38 +01:00
` catchChatError ` ( \ e -> toView ( CRChatError ( Just user ) e ) $> [] )
2023-01-24 17:58:08 +04:00
where
delete ct
| directOrUsed ct = pure []
| otherwise =
withStore' ( \ db -> checkContactHasGroups db user ct ) >>= \ case
Just _ -> pure []
Nothing -> do
conns <- withStore $ \ db -> getContactConnections db userId ct
2023-06-12 13:45:39 +04:00
withStore' ( \ db -> setContactDeleted db user ct )
2023-07-09 23:24:38 +01:00
` catchChatError ` ( toView . CRChatError ( Just user ) )
2023-01-24 17:58:08 +04:00
pure $ map aConnId conns
2023-01-04 21:06:28 +04:00
CTContactRequest -> pure $ chatCmdError ( Just user ) " not supported "
2022-11-15 10:31:44 +04:00
APIClearChat ( ChatRef cType chatId ) -> withUser $ \ user -> case cType of
2022-05-17 11:22:09 +04:00
CTDirect -> do
2022-11-15 10:31:44 +04:00
ct <- withStore $ \ db -> getContact db user chatId
2022-10-04 01:33:36 +04:00
filesInfo <- withStore' $ \ db -> getContactFileInfo db user ct
2023-01-24 16:24:34 +04:00
deleteFilesAndConns user filesInfo
2022-10-04 01:33:36 +04:00
withStore' $ \ db -> deleteContactCIs db user ct
2023-01-17 16:58:36 +04:00
pure $ CRChatCleared user ( AChatInfo SCTDirect $ DirectChat ct )
2022-05-17 11:22:09 +04:00
CTGroup -> do
2022-06-18 20:06:13 +01:00
gInfo <- withStore $ \ db -> getGroupInfo db user chatId
2022-10-04 01:33:36 +04:00
filesInfo <- withStore' $ \ db -> getGroupFileInfo db user gInfo
2023-01-24 16:24:34 +04:00
deleteFilesAndConns user filesInfo
2022-10-04 01:33:36 +04:00
withStore' $ \ db -> deleteGroupCIs db user gInfo
2022-10-26 13:37:17 +04:00
membersToDelete <- withStore' $ \ db -> getGroupMembersForExpiration db user gInfo
forM_ membersToDelete $ \ m -> withStore' $ \ db -> deleteGroupMember db user m
2023-01-17 16:58:36 +04:00
pure $ CRChatCleared user ( AChatInfo SCTGroup $ GroupChat gInfo )
2023-01-04 21:06:28 +04:00
CTContactConnection -> pure $ chatCmdError ( Just user ) " not supported "
CTContactRequest -> pure $ chatCmdError ( Just user ) " not supported "
2023-08-08 17:25:28 +04:00
APIAcceptContact incognito connReqId -> withUser $ \ _ -> withChatLock " acceptContact " $ do
2023-01-20 17:55:57 +04:00
( user , cReq ) <- withStore $ \ db -> getContactRequest' db connReqId
2022-10-13 17:12:22 +04:00
-- [incognito] generate profile to send, create connection with incognito profile
2022-10-14 14:57:01 +04:00
incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing
2022-10-13 17:12:22 +04:00
ct <- acceptContactRequest user cReq incognitoProfile
2023-01-04 21:06:28 +04:00
pure $ CRAcceptingContactRequest user ct
2023-01-20 17:55:57 +04:00
APIRejectContact connReqId -> withUser $ \ user -> withChatLock " rejectContact " $ do
2022-02-01 05:31:34 +00:00
cReq @ UserContactRequest { agentContactConnId = AgentConnId connId , agentInvitationId = AgentInvId invId } <-
2022-06-18 20:06:13 +01:00
withStore $ \ db ->
2023-01-20 17:55:57 +04:00
getContactRequest db user connReqId
2023-07-09 23:24:38 +01:00
` storeFinally ` liftIO ( deleteContactRequest db user connReqId )
2022-02-01 05:31:34 +00:00
withAgent $ \ a -> rejectContact a connId invId
2023-01-04 21:06:28 +04:00
pure $ CRContactRequestRejected user cReq
2022-12-20 12:58:15 +00:00
APISendCallInvitation contactId callType -> withUser $ \ user -> do
2022-05-03 10:22:35 +01:00
-- party initiating call
2022-11-15 10:31:44 +04:00
ct <- withStore $ \ db -> getContact db user contactId
2022-12-03 18:06:21 +00:00
assertDirectAllowed user MDSnd ct XCallInv_
2023-04-17 11:18:04 +02:00
if featureAllowed SCFCalls forUser ct
then do
calls <- asks currentCalls
withChatLock " sendCallInvitation " $ do
callId <- CallId <$> drgRandomBytes 16
dhKeyPair <- if encryptedCall callType then Just <$> liftIO C . generateKeyPair' else pure Nothing
let invitation = CallInvitation { callType , callDhPubKey = fst <$> dhKeyPair }
callState = CallInvitationSent { localCallType = callType , localDhPrivKey = snd <$> dhKeyPair }
( msg , _ ) <- sendDirectContactMessage ct ( XCallInv callId invitation )
ci <- saveSndChatItem user ( CDDirectSnd ct ) msg ( CISndCall CISCallPending 0 )
let call' = Call { contactId , callId , chatItemId = chatItemId' ci , callState , callTs = chatItemTs' ci }
call_ <- atomically $ TM . lookupInsert contactId call' calls
forM_ call_ $ \ call -> updateCallItemStatus user ct call WCSDisconnected Nothing
toView $ CRNewChatItem user ( AChatItem SCTDirect SMDSnd ( DirectChat ct ) ci )
ok user
else pure $ chatCmdError ( Just user ) ( " feature not allowed " <> T . unpack ( chatFeatureNameText CFCalls ) )
2022-09-05 15:23:38 +01:00
SendCallInvitation cName callType -> withUser $ \ user -> do
contactId <- withStore $ \ db -> getContactIdByName db user cName
2022-05-17 08:37:00 +01:00
processChatCommand $ APISendCallInvitation contactId callType
2022-05-03 10:22:35 +01:00
APIRejectCall contactId ->
-- party accepting call
2022-12-20 12:58:15 +00:00
withCurrentCall contactId $ \ user ct Call { chatItemId , callState } -> case callState of
2022-05-28 12:34:40 +01:00
CallInvitationReceived { } -> do
2022-05-04 13:31:00 +01:00
let aciContent = ACIContent SMDRcv $ CIRcvCall CISCallRejected 0
2022-12-20 12:58:15 +00:00
withStore' $ \ db -> updateDirectChatItemsRead db user contactId $ Just ( chatItemId , chatItemId )
updateDirectChatItemView user ct chatItemId aciContent False Nothing $> Nothing
2022-05-03 10:22:35 +01:00
_ -> throwChatError . CECallState $ callStateTag callState
APISendCallOffer contactId WebRTCCallOffer { callType , rtcSession } ->
-- party accepting call
2023-01-16 15:06:03 +04:00
withCurrentCall contactId $ \ user ct call @ Call { callId , chatItemId , callState } -> case callState of
2022-05-03 10:22:35 +01:00
CallInvitationReceived { peerCallType , localDhPubKey , sharedKey } -> do
2022-05-18 07:01:32 +01:00
let callDhPubKey = if encryptedCall callType then localDhPubKey else Nothing
offer = CallOffer { callType , rtcSession , callDhPubKey }
2022-05-03 10:22:35 +01:00
callState' = CallOfferSent { localCallType = callType , peerCallType , localCallSession = rtcSession , sharedKey }
2022-05-04 13:31:00 +01:00
aciContent = ACIContent SMDRcv $ CIRcvCall CISCallAccepted 0
2022-10-14 13:06:33 +01:00
( SndMessage { msgId } , _ ) <- sendDirectContactMessage ct ( XCallOffer callId offer )
2023-01-16 15:06:03 +04:00
withStore' $ \ db -> updateDirectChatItemsRead db user contactId $ Just ( chatItemId , chatItemId )
updateDirectChatItemView user ct chatItemId aciContent False $ Just msgId
2022-05-03 10:22:35 +01:00
pure $ Just call { callState = callState' }
_ -> throwChatError . CECallState $ callStateTag callState
APISendCallAnswer contactId rtcSession ->
-- party initiating call
2023-01-16 15:06:03 +04:00
withCurrentCall contactId $ \ user ct call @ Call { callId , chatItemId , callState } -> case callState of
2022-05-03 10:22:35 +01:00
CallOfferReceived { localCallType , peerCallType , peerCallSession , sharedKey } -> do
let callState' = CallNegotiated { localCallType , peerCallType , localCallSession = rtcSession , peerCallSession , sharedKey }
2022-05-04 13:31:00 +01:00
aciContent = ACIContent SMDSnd $ CISndCall CISCallNegotiated 0
2022-10-14 13:06:33 +01:00
( SndMessage { msgId } , _ ) <- sendDirectContactMessage ct ( XCallAnswer callId CallAnswer { rtcSession } )
2023-01-16 15:06:03 +04:00
updateDirectChatItemView user ct chatItemId aciContent False $ Just msgId
2022-05-03 10:22:35 +01:00
pure $ Just call { callState = callState' }
_ -> throwChatError . CECallState $ callStateTag callState
APISendCallExtraInfo contactId rtcExtraInfo ->
-- any call party
withCurrentCall contactId $ \ _ ct call @ Call { callId , callState } -> case callState of
2022-05-07 06:40:46 +01:00
CallOfferSent { localCallType , peerCallType , localCallSession , sharedKey } -> do
-- TODO update the list of ice servers in localCallSession
2022-10-14 13:06:33 +01:00
void . sendDirectContactMessage ct $ XCallExtra callId CallExtraInfo { rtcExtraInfo }
2022-05-07 06:40:46 +01:00
let callState' = CallOfferSent { localCallType , peerCallType , localCallSession , sharedKey }
pure $ Just call { callState = callState' }
2022-05-03 10:22:35 +01:00
CallNegotiated { localCallType , peerCallType , localCallSession , peerCallSession , sharedKey } -> do
2022-05-07 06:40:46 +01:00
-- TODO update the list of ice servers in localCallSession
2022-10-14 13:06:33 +01:00
void . sendDirectContactMessage ct $ XCallExtra callId CallExtraInfo { rtcExtraInfo }
2022-05-03 10:22:35 +01:00
let callState' = CallNegotiated { localCallType , peerCallType , localCallSession , peerCallSession , sharedKey }
pure $ Just call { callState = callState' }
_ -> throwChatError . CECallState $ callStateTag callState
APIEndCall contactId ->
-- any call party
2023-01-16 15:06:03 +04:00
withCurrentCall contactId $ \ user ct call @ Call { callId } -> do
2022-10-14 13:06:33 +01:00
( SndMessage { msgId } , _ ) <- sendDirectContactMessage ct ( XCallEnd callId )
2023-01-16 15:06:03 +04:00
updateCallItemStatus user ct call WCSDisconnected $ Just msgId
2022-05-03 10:22:35 +01:00
pure Nothing
2023-01-16 15:06:03 +04:00
APIGetCallInvitations -> withUser $ \ _ -> do
2022-07-05 15:15:15 +04:00
calls <- asks currentCalls >>= readTVarIO
let invs = mapMaybe callInvitation $ M . elems calls
2023-01-16 15:06:03 +04:00
rcvCallInvitations <- rights <$> mapM rcvCallInvitation invs
pure $ CRCallInvitations rcvCallInvitations
2022-07-04 11:15:25 +01:00
where
callInvitation Call { contactId , callState , callTs } = case callState of
CallInvitationReceived { peerCallType , sharedKey } -> Just ( contactId , callTs , peerCallType , sharedKey )
_ -> Nothing
2023-01-16 15:06:03 +04:00
rcvCallInvitation ( contactId , callTs , peerCallType , sharedKey ) = runExceptT . withStore $ \ db -> do
user <- getUserByContactId db contactId
contact <- getContact db user contactId
2023-01-19 16:00:41 +00:00
pure RcvCallInvitation { user , contact , callType = peerCallType , sharedKey , callTs }
2022-05-04 13:31:00 +01:00
APICallStatus contactId receivedStatus ->
2023-01-16 15:06:03 +04:00
withCurrentCall contactId $ \ user ct call ->
updateCallItemStatus user ct call receivedStatus Nothing $> Just call
2023-01-13 12:24:54 +00:00
APIUpdateProfile userId profile -> withUserId userId ( ` updateProfile ` profile )
2022-11-15 10:31:44 +04:00
APISetContactPrefs contactId prefs' -> withUser $ \ user -> do
ct <- withStore $ \ db -> getContact db user contactId
2022-11-01 17:32:49 +03:00
updateContactPrefs user ct prefs'
2022-11-15 10:31:44 +04:00
APISetContactAlias contactId localAlias -> withUser $ \ user @ User { userId } -> do
2022-08-24 19:03:43 +04:00
ct' <- withStore $ \ db -> do
2022-11-15 10:31:44 +04:00
ct <- getContact db user contactId
2022-08-24 19:03:43 +04:00
liftIO $ updateContactAlias db userId ct localAlias
2023-01-04 21:06:28 +04:00
pure $ CRContactAliasUpdated user ct'
APISetConnectionAlias connId localAlias -> withUser $ \ user @ User { userId } -> do
2022-09-27 20:45:46 +01:00
conn' <- withStore $ \ db -> do
conn <- getPendingContactConnection db userId connId
liftIO $ updateContactConnectionAlias db userId conn localAlias
2023-01-04 21:06:28 +04:00
pure $ CRConnectionAliasUpdated user conn'
2022-04-04 19:51:49 +01:00
APIParseMarkdown text -> pure . CRApiParsedMarkdown $ parseMaybeMarkdownList text
2022-06-25 17:02:16 +01:00
APIGetNtfToken -> withUser $ \ _ -> crNtfToken <$> withAgent getNtfToken
2023-01-18 10:20:55 +00:00
APIRegisterToken token mode -> withUser $ \ _ ->
CRNtfTokenStatus <$> withAgent ( \ a -> registerNtfToken a token mode )
APIVerifyToken token nonce code -> withUser $ \ _ -> withAgent ( \ a -> verifyNtfToken a token nonce code ) >> ok_
APIDeleteToken token -> withUser $ \ _ -> withAgent ( ` deleteNtfToken ` token ) >> ok_
2023-01-20 10:48:25 +00:00
APIGetNtfMessage nonce encNtfInfo -> withUser $ \ _ -> do
2022-06-19 14:44:13 +01:00
( NotificationInfo { ntfConnId , ntfMsgMeta } , msgs ) <- withAgent $ \ a -> getNotificationMessage a nonce encNtfInfo
let ntfMessages = map ( \ SMP . SMPMsgMeta { msgTs , msgFlags } -> NtfMsgInfo { msgTs = systemToUTCTime msgTs , msgFlags } ) msgs
2023-08-25 04:56:37 +08:00
getMsgTs :: SMP . NMsgMeta -> SystemTime
getMsgTs SMP . NMsgMeta { msgTs } = msgTs
msgTs' = systemToUTCTime . getMsgTs <$> ntfMsgMeta
2023-01-20 10:48:25 +00:00
agentConnId = AgentConnId ntfConnId
user_ <- withStore' ( ` getUserByAConnId ` agentConnId )
connEntity <-
pure user_ $>>= \ user ->
2023-07-09 23:24:38 +01:00
withStore ( \ db -> Just <$> getConnectionEntity db user agentConnId ) ` catchChatError ` ( \ e -> toView ( CRChatError ( Just user ) e ) $> Nothing )
2023-01-20 10:48:25 +00:00
pure CRNtfMessages { user_ , connEntity , msgTs = msgTs' , ntfMessages }
2023-04-05 21:59:12 +01:00
APIGetUserProtoServers userId ( AProtocolType p ) -> withUserId userId $ \ user -> withServerProtocol p $ do
ChatConfig { defaultServers } <- asks config
servers <- withStore' ( ` getProtocolServers ` user )
let defServers = cfgServers p defaultServers
servers' = fromMaybe ( L . map toServerCfg defServers ) $ nonEmpty servers
pure $ CRUserProtoServers user $ AUPS $ UserProtoServers p servers' defServers
2022-11-16 15:37:20 +00:00
where
toServerCfg server = ServerCfg { server , preset = True , tested = Nothing , enabled = True }
2023-04-05 21:59:12 +01:00
GetUserProtoServers aProtocol -> withUser $ \ User { userId } ->
processChatCommand $ APIGetUserProtoServers userId aProtocol
APISetUserProtoServers userId ( APSC p ( ProtoServersConfig servers ) ) -> withUserId userId $ \ user -> withServerProtocol p $
withChatLock " setUserSMPServers " $ do
withStore $ \ db -> overwriteProtocolServers db user servers
cfg <- asks config
withAgent $ \ a -> setProtocolServers a ( aUserId user ) $ activeAgentServers cfg p servers
ok user
SetUserProtoServers serversConfig -> withUser $ \ User { userId } ->
processChatCommand $ APISetUserProtoServers userId serversConfig
APITestProtoServer userId srv @ ( AProtoServerWithAuth p server ) -> withUserId userId $ \ user ->
withServerProtocol p $
CRServerTestResult user srv <$> withAgent ( \ a -> testProtocolServer a ( aUserId user ) server )
TestProtoServer srv -> withUser $ \ User { userId } ->
processChatCommand $ APITestProtoServer userId srv
2023-04-19 15:21:28 +04:00
APISetChatItemTTL userId newTTL_ -> withUser $ \ user -> do
2023-01-13 12:24:54 +00:00
checkSameUser userId user
2022-10-07 13:53:05 +04:00
checkStoreNotChanged $
2022-10-22 21:22:44 +01:00
withChatLock " setChatItemTTL " $ do
2022-10-07 13:53:05 +04:00
case newTTL_ of
Nothing -> do
withStore' $ \ db -> setChatItemTTL db user newTTL_
2023-01-13 21:01:26 +04:00
setExpireCIFlag user False
2022-10-07 13:53:05 +04:00
Just newTTL -> do
oldTTL <- withStore' ( ` getChatItemTTL ` user )
when ( maybe True ( newTTL < ) oldTTL ) $ do
2023-01-13 21:01:26 +04:00
setExpireCIFlag user False
2022-10-07 13:53:05 +04:00
expireChatItems user newTTL True
withStore' $ \ db -> setChatItemTTL db user newTTL_
2023-01-14 17:52:40 +04:00
startExpireCIThread user
2023-01-13 21:01:26 +04:00
whenM chatStarted $ setExpireCIFlag user True
2023-01-18 10:20:55 +00:00
ok user
2023-01-05 20:38:31 +04:00
SetChatItemTTL newTTL_ -> withUser' $ \ User { userId } -> do
processChatCommand $ APISetChatItemTTL userId newTTL_
2023-01-13 12:24:54 +00:00
APIGetChatItemTTL userId -> withUserId userId $ \ user -> do
2023-05-29 15:18:22 +04:00
ttl <- withStoreCtx' ( Just " APIGetChatItemTTL, getChatItemTTL " ) ( ` getChatItemTTL ` user )
2023-01-04 21:06:28 +04:00
pure $ CRChatItemTTL user ttl
2023-01-05 20:38:31 +04:00
GetChatItemTTL -> withUser' $ \ User { userId } -> do
processChatCommand $ APIGetChatItemTTL userId
2023-01-18 10:20:55 +00:00
APISetNetworkConfig cfg -> withUser' $ \ _ -> withAgent ( ` setNetworkConfig ` cfg ) >> ok_
APIGetNetworkConfig -> withUser' $ \ _ ->
CRNetworkConfig <$> withAgent getNetworkConfig
2023-07-05 09:09:56 +01:00
ReconnectAllServers -> withUser' $ \ _ -> withAgent reconnectAllServers >> ok_
2022-11-15 10:31:44 +04:00
APISetChatSettings ( ChatRef cType chatId ) chatSettings -> withUser $ \ user -> case cType of
2022-08-19 15:17:05 +01:00
CTDirect -> do
ct <- withStore $ \ db -> do
2022-11-15 10:31:44 +04:00
ct <- getContact db user chatId
2022-08-19 15:17:05 +01:00
liftIO $ updateContactSettings db user chatId chatSettings
pure ct
withAgent $ \ a -> toggleConnectionNtfs a ( contactConnId ct ) ( enableNtfs chatSettings )
2023-01-18 10:20:55 +00:00
ok user
2022-08-19 15:17:05 +01:00
CTGroup -> do
ms <- withStore $ \ db -> do
Group _ ms <- getGroup db user chatId
liftIO $ updateGroupSettings db user chatId chatSettings
pure ms
2022-08-20 14:47:24 +01:00
forM_ ( filter memberActive ms ) $ \ m -> forM_ ( memberConnId m ) $ \ connId ->
2023-07-09 23:24:38 +01:00
withAgent ( \ a -> toggleConnectionNtfs a connId $ enableNtfs chatSettings ) ` catchChatError ` ( toView . CRChatError ( Just user ) )
2023-01-18 10:20:55 +00:00
ok user
2023-01-04 21:06:28 +04:00
_ -> pure $ chatCmdError ( Just user ) " not supported "
2022-11-15 10:31:44 +04:00
APIContactInfo contactId -> withUser $ \ user @ User { userId } -> do
2022-08-18 11:35:31 +04:00
-- [incognito] print user's incognito profile for this contact
2022-11-15 10:31:44 +04:00
ct @ Contact { activeConn = Connection { customUserProfileId } } <- withStore $ \ db -> getContact db user contactId
2022-08-18 11:35:31 +04:00
incognitoProfile <- forM customUserProfileId $ \ profileId -> withStore ( \ db -> getProfileById db userId profileId )
connectionStats <- withAgent ( ` getConnectionServers ` contactConnId ct )
2023-01-04 21:06:28 +04:00
pure $ CRContactInfo user ct connectionStats ( fmap fromLocalProfile incognitoProfile )
2023-08-06 11:56:40 +01:00
APIGroupInfo gId -> withUser $ \ user -> do
( g , s ) <- withStore $ \ db -> ( , ) <$> getGroupInfo db user gId <*> liftIO ( getGroupSummary db user gId )
pure $ CRGroupInfo user g s
2022-08-27 19:56:03 +04:00
APIGroupMemberInfo gId gMemberId -> withUser $ \ user -> do
( g , m ) <- withStore $ \ db -> ( , ) <$> getGroupInfo db user gId <*> getGroupMember db user gId gMemberId
2022-08-18 11:35:31 +04:00
connectionStats <- mapM ( withAgent . flip getConnectionServers ) ( memberConnId m )
2023-01-04 21:06:28 +04:00
pure $ CRGroupMemberInfo user g m connectionStats
2022-11-15 10:31:44 +04:00
APISwitchContact contactId -> withUser $ \ user -> do
ct <- withStore $ \ db -> getContact db user contactId
2023-06-19 16:07:17 +04:00
connectionStats <- withAgent $ \ a -> switchConnectionAsync a " " $ contactConnId ct
pure $ CRContactSwitchStarted user ct connectionStats
2022-11-01 13:26:08 +00:00
APISwitchGroupMember gId gMemberId -> withUser $ \ user -> do
2023-06-19 16:07:17 +04:00
( g , m ) <- withStore $ \ db -> ( , ) <$> getGroupInfo db user gId <*> getGroupMember db user gId gMemberId
2022-11-01 13:26:08 +00:00
case memberConnId m of
2023-06-19 16:07:17 +04:00
Just connId -> do
connectionStats <- withAgent ( \ a -> switchConnectionAsync a " " connId )
pure $ CRGroupMemberSwitchStarted user g m connectionStats
2022-11-01 13:26:08 +00:00
_ -> throwChatError CEGroupMemberNotActive
2023-06-16 19:05:53 +04:00
APIAbortSwitchContact contactId -> withUser $ \ user -> do
ct <- withStore $ \ db -> getContact db user contactId
connectionStats <- withAgent $ \ a -> abortConnectionSwitch a $ contactConnId ct
pure $ CRContactSwitchAborted user ct connectionStats
APIAbortSwitchGroupMember gId gMemberId -> withUser $ \ user -> do
( g , m ) <- withStore $ \ db -> ( , ) <$> getGroupInfo db user gId <*> getGroupMember db user gId gMemberId
case memberConnId m of
Just connId -> do
connectionStats <- withAgent $ \ a -> abortConnectionSwitch a connId
pure $ CRGroupMemberSwitchAborted user g m connectionStats
_ -> throwChatError CEGroupMemberNotActive
2023-07-05 19:44:21 +04:00
APISyncContactRatchet contactId force -> withUser $ \ user -> do
ct <- withStore $ \ db -> getContact db user contactId
cStats @ ConnectionStats { ratchetSyncState = rss } <- withAgent $ \ a -> synchronizeRatchet a ( contactConnId ct ) force
createInternalChatItem user ( CDDirectSnd ct ) ( CISndConnEvent $ SCERatchetSync rss Nothing ) Nothing
pure $ CRContactRatchetSyncStarted user ct cStats
APISyncGroupMemberRatchet gId gMemberId force -> withUser $ \ user -> do
( g , m ) <- withStore $ \ db -> ( , ) <$> getGroupInfo db user gId <*> getGroupMember db user gId gMemberId
case memberConnId m of
Just connId -> do
cStats @ ConnectionStats { ratchetSyncState = rss } <- withAgent $ \ a -> synchronizeRatchet a connId force
createInternalChatItem user ( CDGroupSnd g ) ( CISndConnEvent . SCERatchetSync rss . Just $ groupMemberRef m ) Nothing
pure $ CRGroupMemberRatchetSyncStarted user g m cStats
_ -> throwChatError CEGroupMemberNotActive
2022-12-09 15:26:43 +00:00
APIGetContactCode contactId -> withUser $ \ user -> do
ct @ Contact { activeConn = conn @ Connection { connId } } <- withStore $ \ db -> getContact db user contactId
code <- getConnectionCode ( contactConnId ct )
ct' <- case contactSecurityCode ct of
Just SecurityCode { securityCode }
| sameVerificationCode code securityCode -> pure ct
| otherwise -> do
withStore' $ \ db -> setConnectionVerified db user connId Nothing
pure ( ct :: Contact ) { activeConn = conn { connectionCode = Nothing } }
_ -> pure ct
2023-01-04 21:06:28 +04:00
pure $ CRContactCode user ct' code
2022-12-09 15:26:43 +00:00
APIGetGroupMemberCode gId gMemberId -> withUser $ \ user -> do
( g , m @ GroupMember { activeConn } ) <- withStore $ \ db -> ( , ) <$> getGroupInfo db user gId <*> getGroupMember db 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
withStore' $ \ db -> setConnectionVerified db user connId Nothing
pure ( m :: GroupMember ) { activeConn = Just $ ( conn :: Connection ) { connectionCode = Nothing } }
_ -> pure m
2023-01-04 21:06:28 +04:00
pure $ CRGroupMemberCode user g m' code
2022-12-09 15:26:43 +00:00
_ -> throwChatError CEGroupMemberNotActive
APIVerifyContact contactId code -> withUser $ \ user -> do
Contact { activeConn } <- withStore $ \ db -> getContact db user contactId
verifyConnectionCode user activeConn code
APIVerifyGroupMember gId gMemberId code -> withUser $ \ user -> do
GroupMember { activeConn } <- withStore $ \ db -> getGroupMember db user gId gMemberId
case activeConn of
Just conn -> verifyConnectionCode user conn code
_ -> throwChatError CEGroupMemberNotActive
2023-01-07 19:47:51 +04:00
APIEnableContact contactId -> withUser $ \ user -> do
Contact { activeConn } <- withStore $ \ db -> getContact db user contactId
withStore' $ \ db -> setConnectionAuthErrCounter db user activeConn 0
2023-01-18 10:20:55 +00:00
ok user
2023-01-07 19:47:51 +04:00
APIEnableGroupMember gId gMemberId -> withUser $ \ user -> do
GroupMember { activeConn } <- withStore $ \ db -> getGroupMember db user gId gMemberId
case activeConn of
Just conn -> do
withStore' $ \ db -> setConnectionAuthErrCounter db user conn 0
2023-01-18 10:20:55 +00:00
ok user
2023-01-07 19:47:51 +04:00
_ -> throwChatError CEGroupMemberNotActive
2023-07-13 23:48:25 +01:00
SetShowMessages cName ntfOn -> updateChatSettings cName ( \ cs -> cs { enableNtfs = ntfOn } )
SetSendReceipts cName rcptsOn_ -> updateChatSettings cName ( \ cs -> cs { sendRcpts = rcptsOn_ } )
2022-12-09 15:26:43 +00:00
ContactInfo cName -> withContactName cName APIContactInfo
2023-08-06 11:56:40 +01:00
ShowGroupInfo gName -> withUser $ \ user -> do
groupId <- withStore $ \ db -> getGroupIdByName db user gName
processChatCommand $ APIGroupInfo groupId
2022-12-09 15:26:43 +00:00
GroupMemberInfo gName mName -> withMemberName gName mName APIGroupMemberInfo
SwitchContact cName -> withContactName cName APISwitchContact
SwitchGroupMember gName mName -> withMemberName gName mName APISwitchGroupMember
2023-06-16 19:05:53 +04:00
AbortSwitchContact cName -> withContactName cName APIAbortSwitchContact
AbortSwitchGroupMember gName mName -> withMemberName gName mName APIAbortSwitchGroupMember
2023-07-05 19:44:21 +04:00
SyncContactRatchet cName force -> withContactName cName $ \ ctId -> APISyncContactRatchet ctId force
SyncGroupMemberRatchet gName mName force -> withMemberName gName mName $ \ gId mId -> APISyncGroupMemberRatchet gId mId force
2022-12-09 15:26:43 +00:00
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
2023-01-07 19:47:51 +04:00
EnableContact cName -> withContactName cName APIEnableContact
EnableGroupMember gName mName -> withMemberName gName mName $ \ gId mId -> APIEnableGroupMember gId mId
2022-01-24 16:07:17 +00:00
ChatHelp section -> pure $ CRChatHelp section
2022-02-06 16:18:01 +00:00
Welcome -> withUser $ pure . CRWelcome
2023-08-08 17:25:28 +04:00
APIAddContact userId incognito -> withUserId userId $ \ user -> withChatLock " addContact " . procCmd $ do
2022-08-18 11:35:31 +04:00
-- [incognito] generate profile for connection
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
2023-09-10 22:40:15 +03:00
subMode <- chatReadVar subscriptionMode
( connId , cReq ) <- withAgent $ \ a -> createConnection a ( aUserId user ) True SCMInvitation Nothing subMode
conn <- withStore' $ \ db -> createDirectConnection db user connId cReq ConnNew incognitoProfile subMode
2023-01-04 21:06:28 +04:00
toView $ CRNewContactConnection user conn
2023-08-08 17:25:28 +04:00
pure $ CRInvitation user cReq conn
AddContact incognito -> withUser $ \ User { userId } ->
processChatCommand $ APIAddContact userId incognito
APISetConnectionIncognito connId incognito -> withUser $ \ user @ User { userId } -> do
conn'_ <- withStore $ \ db -> do
conn @ PendingContactConnection { pccConnStatus , customUserProfileId } <- getPendingContactConnection db userId connId
case ( pccConnStatus , customUserProfileId , incognito ) of
( ConnNew , Nothing , True ) -> liftIO $ do
incognitoProfile <- generateRandomProfile
pId <- createIncognitoProfile db user incognitoProfile
Just <$> updatePCCIncognito db user conn ( Just pId )
( ConnNew , Just pId , False ) -> liftIO $ do
deletePCCIncognitoProfile db user pId
Just <$> updatePCCIncognito db user conn Nothing
_ -> pure Nothing
case conn'_ of
Just conn' -> pure $ CRConnectionIncognitoUpdated user conn'
Nothing -> throwChatError CEConnectionIncognitoChangeProhibited
APIConnect userId incognito ( Just ( ACR SCMInvitation cReq ) ) -> withUserId userId $ \ user -> withChatLock " connect " . procCmd $ do
2023-09-10 22:40:15 +03:00
subMode <- chatReadVar subscriptionMode
2022-08-18 11:35:31 +04:00
-- [incognito] generate profile to send
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
2022-11-01 17:32:49 +03:00
let profileToSend = userProfileToSend user incognitoProfile Nothing
2023-09-01 19:20:07 +04:00
dm <- directMessage $ XInfo profileToSend
2023-09-10 22:40:15 +03:00
connId <- withAgent $ \ a -> joinConnection a ( aUserId user ) True cReq dm subMode
conn <- withStore' $ \ db -> createDirectConnection db user connId cReq ConnJoined ( incognitoProfile $> profileToSend ) subMode
2023-01-04 21:06:28 +04:00
toView $ CRNewContactConnection user conn
pure $ CRSentConfirmation user
2023-08-08 17:25:28 +04:00
APIConnect userId incognito ( Just ( ACR SCMContact cReq ) ) -> withUserId userId $ \ user -> connectViaContact user incognito cReq
APIConnect _ _ Nothing -> throwChatError CEInvalidConnReq
Connect incognito cReqUri -> withUser $ \ User { userId } ->
processChatCommand $ APIConnect userId incognito cReqUri
ConnectSimplex incognito -> withUser $ \ user ->
2022-08-18 11:35:31 +04:00
-- [incognito] generate profile to send
2023-08-08 17:25:28 +04:00
connectViaContact user incognito adminContactReq
2022-12-09 15:26:43 +00:00
DeleteContact cName -> withContactName cName $ APIDeleteChat . ChatRef CTDirect
ClearContact cName -> withContactName cName $ APIClearChat . ChatRef CTDirect
2023-01-13 12:24:54 +00:00
APIListContacts userId -> withUserId userId $ \ user ->
CRContactsList user <$> withStore' ( ` getUserContacts ` user )
2023-01-05 20:38:31 +04:00
ListContacts -> withUser $ \ User { userId } ->
processChatCommand $ APIListContacts userId
2023-01-13 12:24:54 +00:00
APICreateMyAddress userId -> withUserId userId $ \ user -> withChatLock " createMyAddress " . procCmd $ do
2023-09-10 22:40:15 +03:00
subMode <- chatReadVar subscriptionMode
( connId , cReq ) <- withAgent $ \ a -> createConnection a ( aUserId user ) True SCMContact Nothing subMode
withStore $ \ db -> createUserContactLink db user connId cReq subMode
2023-01-04 21:06:28 +04:00
pure $ CRUserContactLinkCreated user cReq
2023-01-05 20:38:31 +04:00
CreateMyAddress -> withUser $ \ User { userId } ->
processChatCommand $ APICreateMyAddress userId
2023-04-27 17:19:21 +04:00
APIDeleteMyAddress userId -> withUserId userId $ \ user @ User { profile = p } -> do
2022-10-13 17:12:22 +04:00
conns <- withStore ( ` getUserAddressConnections ` user )
2023-04-27 17:19:21 +04:00
withChatLock " deleteMyAddress " $ do
2023-01-24 16:24:34 +04:00
deleteAgentConnectionsAsync user $ map aConnId conns
2022-10-13 17:12:22 +04:00
withStore' ( ` deleteUserAddress ` user )
2023-04-27 17:19:21 +04:00
let p' = ( fromLocalProfile p :: Profile ) { contactLink = Nothing }
r <- updateProfile_ user p' $ withStore' $ \ db -> setUserProfileContactLink db user Nothing
let user' = case r of
2023-08-22 16:13:57 +01:00
CRUserProfileUpdated u' _ _ _ -> u'
2023-04-27 17:19:21 +04:00
_ -> user
pure $ CRUserContactLinkDeleted user'
2023-01-05 20:38:31 +04:00
DeleteMyAddress -> withUser $ \ User { userId } ->
processChatCommand $ APIDeleteMyAddress userId
2023-01-18 10:20:55 +00:00
APIShowMyAddress userId -> withUserId userId $ \ user ->
2023-05-29 15:18:22 +04:00
CRUserContactLink user <$> withStoreCtx ( Just " APIShowMyAddress, getUserAddress " ) ( ` getUserAddress ` user )
2023-01-05 20:38:31 +04:00
ShowMyAddress -> withUser $ \ User { userId } ->
processChatCommand $ APIShowMyAddress userId
2023-04-27 17:19:21 +04:00
APISetProfileAddress userId False -> withUserId userId $ \ user @ User { profile = p } -> do
let p' = ( fromLocalProfile p :: Profile ) { contactLink = Nothing }
updateProfile_ user p' $ withStore' $ \ db -> setUserProfileContactLink db user Nothing
APISetProfileAddress userId True -> withUserId userId $ \ user @ User { profile = p } -> do
ucl @ UserContactLink { connReqContact } <- withStore ( ` getUserAddress ` user )
let p' = ( fromLocalProfile p :: Profile ) { contactLink = Just connReqContact }
updateProfile_ user p' $ withStore' $ \ db -> setUserProfileContactLink db user $ Just ucl
SetProfileAddress onOff -> withUser $ \ User { userId } ->
processChatCommand $ APISetProfileAddress userId onOff
2023-01-13 12:24:54 +00:00
APIAddressAutoAccept userId autoAccept_ -> withUserId userId $ \ user -> do
contactLink <- withStore ( \ db -> updateUserAddressAutoAccept db user autoAccept_ )
2023-01-04 21:06:28 +04:00
pure $ CRUserContactLinkUpdated user contactLink
2023-01-05 20:38:31 +04:00
AddressAutoAccept autoAccept_ -> withUser $ \ User { userId } ->
processChatCommand $ APIAddressAutoAccept userId autoAccept_
2023-08-08 17:25:28 +04:00
AcceptContact incognito cName -> withUser $ \ User { userId } -> do
2022-06-18 20:06:13 +01:00
connReqId <- withStore $ \ db -> getContactRequestIdByName db userId cName
2023-08-08 17:25:28 +04:00
processChatCommand $ APIAcceptContact incognito connReqId
2022-02-06 16:18:01 +00:00
RejectContact cName -> withUser $ \ User { userId } -> do
2022-06-18 20:06:13 +01:00
connReqId <- withStore $ \ db -> getContactRequestIdByName db userId cName
2022-02-06 16:18:01 +00:00
processChatCommand $ APIRejectContact connReqId
2023-09-16 21:30:20 +04:00
SendMessage ( ChatName cType name ) msg -> withUser $ \ user -> do
let mc = MCText msg
case cType of
CTDirect ->
withStore' ( \ db -> runExceptT $ getContactIdByName db user name ) >>= \ case
Right ctId -> do
let chatRef = ChatRef CTDirect ctId
processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage Nothing Nothing mc
Left _ ->
withStore' ( \ db -> runExceptT $ getActiveMembersByName db user name ) >>= \ case
Right [ ( gInfo , member ) ] -> do
let GroupInfo { localDisplayName = gName } = gInfo
GroupMember { localDisplayName = mName } = member
processChatCommand $ SendMemberContactMessage gName mName msg
Right ( suspectedMember : _ ) ->
throwChatError $ CEContactNotFound name ( Just suspectedMember )
_ ->
throwChatError $ CEContactNotFound name Nothing
CTGroup -> do
gId <- withStore $ \ db -> getGroupIdByName db user name
let chatRef = ChatRef CTGroup gId
processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage Nothing Nothing mc
_ -> throwChatError $ CECommandError " not supported "
SendMemberContactMessage gName mName msg -> withUser $ \ user -> do
( gId , mId ) <- getGroupAndMemberId user gName mName
m <- withStore $ \ db -> getGroupMember db user gId mId
let mc = MCText msg
case memberContactId m of
Nothing -> do
gInfo <- withStore $ \ db -> getGroupInfo db user gId
toView $ CRNoMemberContactCreating user gInfo m
processChatCommand ( APICreateMemberContact gId mId ) >>= \ case
cr @ ( CRNewMemberContact _ Contact { contactId } _ _ ) -> do
toView cr
processChatCommand $ APISendMemberContactInvitation contactId ( Just mc )
cr -> pure cr
Just ctId -> do
let chatRef = ChatRef CTDirect ctId
processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage Nothing Nothing mc
SendLiveMessage chatName msg -> withUser $ \ user -> do
chatRef <- getChatRef user chatName
let mc = MCText msg
processChatCommand . APISendMessage chatRef True Nothing $ ComposedMessage Nothing Nothing mc
2022-03-29 08:53:30 +01:00
SendMessageBroadcast msg -> withUser $ \ user -> do
2022-06-18 20:06:13 +01:00
contacts <- withStore' ( ` getUserContacts ` user )
2023-06-17 10:34:04 +01:00
let cts = filter ( \ ct -> isReady ct && directOrUsed ct ) contacts
ChatConfig { logLevel } <- asks config
2022-10-22 21:22:44 +01:00
withChatLock " sendMessageBroadcast " . procCmd $ do
2023-06-17 10:34:04 +01:00
( successes , failures ) <- foldM ( sendAndCount user logLevel ) ( 0 , 0 ) cts
timestamp <- liftIO getCurrentTime
pure CRBroadcastSent { user , msgContent = mc , successes , failures , timestamp }
where
mc = MCText msg
sendAndCount user ll ( s , f ) ct =
2023-07-09 23:24:38 +01:00
( sendToContact user ct $> ( s + 1 , f ) ) ` catchChatError ` \ e -> when ( ll <= CLLInfo ) ( toView $ CRChatError ( Just user ) e ) $> ( s , f + 1 )
2023-06-17 10:34:04 +01:00
sendToContact user ct = do
( sndMsg , _ ) <- sendDirectContactMessage ct ( XMsgNew $ MCSimple ( extMsgContent mc Nothing ) )
void $ saveSndChatItem user ( CDDirectSnd ct ) sndMsg ( CISndMsgContent mc )
2022-09-05 15:23:38 +01:00
SendMessageQuote cName ( AMsgDirection msgDir ) quotedMsg msg -> withUser $ \ user @ User { userId } -> do
contactId <- withStore $ \ db -> getContactIdByName db user cName
2023-02-18 15:16:50 +00:00
quotedItemId <- withStore $ \ db -> getDirectChatItemIdByText db userId contactId msgDir quotedMsg
let mc = MCText msg
2023-05-11 16:00:01 +04:00
processChatCommand . APISendMessage ( ChatRef CTDirect contactId ) False Nothing $ ComposedMessage Nothing ( Just quotedItemId ) mc
2022-04-28 08:34:21 +01:00
DeleteMessage chatName deletedMsg -> withUser $ \ user -> do
chatRef <- getChatRef user chatName
deletedItemId <- getSentChatItemIdByText user chatRef deletedMsg
processChatCommand $ APIDeleteChatItem chatRef deletedItemId CIDMBroadcast
2023-02-08 07:08:53 +00:00
DeleteMemberMessage gName mName deletedMsg -> withUser $ \ user -> do
( gId , mId ) <- getGroupAndMemberId user gName mName
2023-02-18 15:16:50 +00:00
deletedItemId <- withStore $ \ db -> getGroupChatItemIdByText db user gId ( Just mName ) deletedMsg
2023-02-08 07:08:53 +00:00
processChatCommand $ APIDeleteMemberChatItem gId mId deletedItemId
2022-04-28 08:34:21 +01:00
EditMessage chatName editedMsg msg -> withUser $ \ user -> do
chatRef <- getChatRef user chatName
editedItemId <- getSentChatItemIdByText user chatRef editedMsg
2023-02-18 15:16:50 +00:00
let mc = MCText msg
2022-12-16 07:51:04 +00:00
processChatCommand $ APIUpdateChatItem chatRef editedItemId False mc
2022-12-19 11:16:50 +00:00
UpdateLiveMessage chatName chatItemId live msg -> withUser $ \ user -> do
chatRef <- getChatRef user chatName
2023-02-18 15:16:50 +00:00
let mc = MCText msg
2022-12-19 11:16:50 +00:00
processChatCommand $ APIUpdateChatItem chatRef chatItemId live mc
2023-05-23 13:51:23 +04:00
ReactToMessage add reaction chatName msg -> withUser $ \ user -> do
2023-05-15 12:28:53 +02:00
chatRef <- getChatRef user chatName
chatItemId <- getChatItemIdByText user chatRef msg
2023-05-17 01:22:00 +02:00
processChatCommand $ APIChatItemReaction chatRef chatItemId add reaction
2023-01-13 12:24:54 +00:00
APINewGroup userId gProfile -> withUserId userId $ \ user -> do
2021-07-12 19:00:03 +01:00
gVar <- asks idsDrg
2023-01-13 12:24:54 +00:00
groupInfo <- withStore $ \ db -> createNewGroup db gVar user gProfile
2023-01-04 21:06:28 +04:00
pure $ CRGroupCreated user groupInfo
2023-01-05 20:38:31 +04:00
NewGroup gProfile -> withUser $ \ User { userId } ->
processChatCommand $ APINewGroup userId gProfile
2022-11-15 10:31:44 +04:00
APIAddMember groupId contactId memRole -> withUser $ \ user -> withChatLock " addMember " $ do
2022-01-26 16:18:27 +04:00
-- TODO for large groups: no need to load all members to determine if contact is a member
2022-11-15 10:31:44 +04:00
( group , contact ) <- withStore $ \ db -> ( , ) <$> getGroup db user groupId <*> getContact db user contactId
2022-12-03 18:06:21 +00:00
assertDirectAllowed user MDSnd contact XGrpInv_
2023-09-19 18:50:10 +01:00
let Group gInfo members = group
2022-07-12 19:20:56 +04:00
Contact { localDisplayName = cName } = contact
2023-02-01 13:57:39 +00:00
assertUserGroupRole gInfo $ max GRAdmin memRole
2022-08-27 19:56:03 +04:00
-- [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
2023-09-19 18:50:10 +01:00
when ( incognitoMembership gInfo ) $ throwChatError CEGroupIncognitoCantInvite
2022-10-03 09:00:47 +01:00
let sendInvitation = sendGrpInvitation user contact gInfo
2022-01-06 23:39:58 +04:00
case contactMember contact members of
Nothing -> do
gVar <- asks idsDrg
2023-09-10 22:40:15 +03:00
subMode <- chatReadVar subscriptionMode
( agentConnId , cReq ) <- withAgent $ \ a -> createConnection a ( aUserId user ) True SCMInvitation Nothing subMode
member <- withStore $ \ db -> createNewContactMember db gVar user groupId contact memRole agentConnId cReq subMode
2022-08-09 13:43:19 +04:00
sendInvitation member cReq
2023-01-04 21:06:28 +04:00
pure $ CRSentGroupInvitation user gInfo contact member
2022-11-09 14:12:42 +04:00
Just member @ GroupMember { groupMemberId , memberStatus , memberRole = mRole }
| memberStatus == GSMemInvited -> do
unless ( mRole == memRole ) $ withStore' $ \ db -> updateGroupMemberRole db user member memRole
2022-10-15 14:48:07 +04:00
withStore' ( \ db -> getMemberInvitation db user groupMemberId ) >>= \ case
2023-01-04 21:06:28 +04:00
Just cReq -> do
sendInvitation member { memberRole = memRole } cReq
pure $ CRSentGroupInvitation user gInfo contact member { memberRole = memRole }
2022-10-15 14:48:07 +04:00
Nothing -> throwChatError $ CEGroupCantResendInvitation gInfo cName
2022-01-26 21:20:08 +00:00
| otherwise -> throwChatError $ CEGroupDuplicateMember cName
2022-07-12 19:20:56 +04:00
APIJoinGroup groupId -> withUser $ \ user @ User { userId } -> do
2023-09-05 20:15:50 +04:00
( invitation , ct ) <- withStore $ \ db -> do
inv @ ReceivedGroupInvitation { fromMember } <- getGroupInvitation db user groupId
( inv , ) <$> getContactViaMember db user fromMember
let ReceivedGroupInvitation { fromMember , connRequest , groupInfo = g @ GroupInfo { membership } } = invitation
2023-09-06 11:41:23 +04:00
Contact { activeConn = Connection { peerChatVRange } } = ct
2022-10-22 21:22:44 +01:00
withChatLock " joinGroup " . procCmd $ do
2023-09-10 22:40:15 +03:00
subMode <- chatReadVar subscriptionMode
2023-09-10 21:11:35 +01:00
dm <- directMessage $ XGrpAcpt membership . memberId
2023-09-10 22:40:15 +03:00
agentConnId <- withAgent $ \ a -> joinConnection a ( aUserId user ) True connRequest dm subMode
2022-06-18 20:06:13 +01:00
withStore' $ \ db -> do
2023-09-12 17:59:09 +04:00
createMemberConnection db userId fromMember agentConnId ( fromJVersionRange peerChatVRange ) subMode
2022-06-18 20:06:13 +01:00
updateGroupMemberStatus db userId fromMember GSMemAccepted
2022-08-27 19:56:03 +04:00
updateGroupMemberStatus db userId membership GSMemAccepted
2022-07-15 17:49:29 +04:00
updateCIGroupInvitationStatus user
2023-01-04 21:06:28 +04:00
pure $ CRUserAcceptedGroupSent user g { membership = membership { memberStatus = GSMemAccepted } } Nothing
2022-07-15 17:49:29 +04:00
where
2022-12-20 12:58:15 +00:00
updateCIGroupInvitationStatus user = do
2022-07-15 17:49:29 +04:00
AChatItem _ _ cInfo ChatItem { content , meta = CIMeta { itemId } } <- withStore $ \ db -> getChatItemByGroupId db user groupId
case ( cInfo , content ) of
( DirectChat ct , CIRcvGroupInvitation ciGroupInv memRole ) -> do
let aciContent = ACIContent SMDRcv $ CIRcvGroupInvitation ciGroupInv { status = CIGISAccepted } memRole
2022-12-20 12:58:15 +00:00
updateDirectChatItemView user ct itemId aciContent False Nothing
2022-07-15 17:49:29 +04:00
_ -> pure () -- prohibited
2022-10-03 09:00:47 +01:00
APIMemberRole groupId memberId memRole -> withUser $ \ user -> do
Group gInfo @ GroupInfo { membership } members <- withStore $ \ db -> getGroup db user groupId
if memberId == groupMemberId' membership
then changeMemberRole user gInfo members membership $ SGEUserRole memRole
else case find ( ( == memberId ) . groupMemberId' ) members of
Just m -> changeMemberRole user gInfo members m $ SGEMemberRole memberId ( fromLocalProfile $ memberProfile m ) memRole
_ -> throwChatError CEGroupMemberNotFound
where
2023-02-01 13:57:39 +00:00
changeMemberRole user gInfo members m gEvent = do
2022-10-03 09:00:47 +01:00
let GroupMember { memberId = mId , memberRole = mRole , memberStatus = mStatus , memberContactId , localDisplayName = cName } = m
2023-02-01 13:57:39 +00:00
assertUserGroupRole gInfo $ maximum [ GRAdmin , mRole , memRole ]
2022-10-22 21:22:44 +01:00
withChatLock " memberRole " . procCmd $ do
2022-10-03 09:00:47 +01:00
unless ( mRole == memRole ) $ do
withStore' $ \ db -> updateGroupMemberRole db user m memRole
case mStatus of
GSMemInvited -> do
2022-11-15 10:31:44 +04:00
withStore ( \ db -> ( , ) <$> mapM ( getContact db user ) memberContactId <*> liftIO ( getMemberInvitation db user $ groupMemberId' m ) ) >>= \ case
2022-10-03 09:00:47 +01:00
( Just ct , Just cReq ) -> sendGrpInvitation user ct gInfo ( m :: GroupMember ) { memberRole = memRole } cReq
_ -> throwChatError $ CEGroupCantResendInvitation gInfo cName
_ -> do
2023-07-26 14:49:35 +04:00
( msg , _ ) <- sendGroupMessage user gInfo members $ XGrpMemRole mId memRole
2022-12-16 07:51:04 +00:00
ci <- saveSndChatItem user ( CDGroupSnd gInfo ) msg ( CISndGroupEvent gEvent )
2023-01-04 21:06:28 +04:00
toView $ CRNewChatItem user ( AChatItem SCTGroup SMDSnd ( GroupChat gInfo ) ci )
pure CRMemberRoleUser { user , groupInfo = gInfo , member = m { memberRole = memRole } , fromRole = mRole , toRole = memRole }
2022-12-06 17:12:39 +04:00
APIRemoveMember groupId memberId -> withUser $ \ user -> do
2023-02-01 13:57:39 +00:00
Group gInfo members <- withStore $ \ db -> getGroup db user groupId
2022-07-15 17:49:29 +04:00
case find ( ( == memberId ) . groupMemberId' ) members of
2022-07-12 19:20:56 +04:00
Nothing -> throwChatError CEGroupMemberNotFound
2022-07-20 16:56:55 +04:00
Just m @ GroupMember { memberId = mId , memberRole = mRole , memberStatus = mStatus , memberProfile } -> do
2023-02-01 13:57:39 +00:00
assertUserGroupRole gInfo $ max GRAdmin mRole
2022-10-22 21:22:44 +01:00
withChatLock " removeMember " . procCmd $ do
2022-08-04 18:39:31 +01:00
case mStatus of
GSMemInvited -> do
2022-09-30 16:18:43 +04:00
deleteMemberConnection user m
2022-08-04 18:39:31 +01:00
withStore' $ \ db -> deleteGroupMember db user m
_ -> do
2023-07-26 14:49:35 +04:00
( msg , _ ) <- sendGroupMessage user gInfo members $ XGrpMemDel mId
2022-12-16 07:51:04 +00:00
ci <- saveSndChatItem user ( CDGroupSnd gInfo ) msg ( CISndGroupEvent $ SGEMemberDeleted memberId ( fromLocalProfile memberProfile ) )
2023-01-04 21:06:28 +04:00
toView $ CRNewChatItem user ( AChatItem SCTGroup SMDSnd ( GroupChat gInfo ) ci )
2022-09-30 16:18:43 +04:00
deleteMemberConnection user m
2022-12-06 17:12:39 +04:00
-- undeleted "member connected" chat item will prevent deletion of member record
deleteOrUpdateMemberRecord user m
2023-01-04 21:06:28 +04:00
pure $ CRUserDeletedMember user gInfo m { memberStatus = GSMemRemoved }
2022-07-12 19:20:56 +04:00
APILeaveGroup groupId -> withUser $ \ user @ User { userId } -> do
Group gInfo @ GroupInfo { membership } members <- withStore $ \ db -> getGroup db user groupId
2022-10-22 21:22:44 +01:00
withChatLock " leaveGroup " . procCmd $ do
2023-07-26 14:49:35 +04:00
( msg , _ ) <- sendGroupMessage user gInfo members XGrpLeave
2022-12-16 07:51:04 +00:00
ci <- saveSndChatItem user ( CDGroupSnd gInfo ) msg ( CISndGroupEvent SGEUserLeft )
2023-01-04 21:06:28 +04:00
toView $ CRNewChatItem user ( AChatItem SCTGroup SMDSnd ( GroupChat gInfo ) ci )
2022-07-19 18:21:15 +04:00
-- TODO delete direct connections that were unused
2023-02-01 13:57:39 +00:00
deleteGroupLinkIfExists user gInfo
2022-12-06 17:12:39 +04:00
-- member records are not deleted to keep history
2023-01-24 16:24:34 +04:00
deleteMembersConnections user members
2022-06-18 20:06:13 +01:00
withStore' $ \ db -> updateGroupMemberStatus db userId membership GSMemLeft
2023-01-04 21:06:28 +04:00
pure $ CRLeftMemberUser user gInfo { membership = membership { memberStatus = GSMemLeft } }
2023-01-18 10:20:55 +00:00
APIListMembers groupId -> withUser $ \ user ->
CRGroupMembers user <$> withStore ( \ db -> getGroup db user groupId )
2022-09-05 15:23:38 +01:00
AddMember gName cName memRole -> withUser $ \ user -> do
( groupId , contactId ) <- withStore $ \ db -> ( , ) <$> getGroupIdByName db user gName <*> getContactIdByName db user cName
2022-07-12 19:20:56 +04:00
processChatCommand $ APIAddMember groupId contactId memRole
JoinGroup gName -> withUser $ \ user -> do
groupId <- withStore $ \ db -> getGroupIdByName db user gName
processChatCommand $ APIJoinGroup groupId
2022-12-09 15:26:43 +00:00
MemberRole gName gMemberName memRole -> withMemberName gName gMemberName $ \ gId gMemberId -> APIMemberRole gId gMemberId memRole
RemoveMember gName gMemberName -> withMemberName gName gMemberName APIRemoveMember
2022-07-12 19:20:56 +04:00
LeaveGroup gName -> withUser $ \ user -> do
groupId <- withStore $ \ db -> getGroupIdByName db user gName
processChatCommand $ APILeaveGroup groupId
2022-02-06 16:18:01 +00:00
DeleteGroup gName -> withUser $ \ user -> do
2022-07-12 19:20:56 +04:00
groupId <- withStore $ \ db -> getGroupIdByName db user gName
processChatCommand $ APIDeleteChat ( ChatRef CTGroup groupId )
2022-05-17 11:22:09 +04:00
ClearGroup gName -> withUser $ \ user -> do
2022-06-18 20:06:13 +01:00
groupId <- withStore $ \ db -> getGroupIdByName db user gName
2022-05-17 11:22:09 +04:00
processChatCommand $ APIClearChat ( ChatRef CTGroup groupId )
2022-07-12 19:20:56 +04:00
ListMembers gName -> withUser $ \ user -> do
groupId <- withStore $ \ db -> getGroupIdByName db user gName
processChatCommand $ APIListMembers groupId
2023-08-01 20:54:51 +01:00
APIListGroups userId contactId_ search_ -> withUserId userId $ \ user ->
2023-08-06 11:56:40 +01:00
CRGroupsList user <$> withStore' ( \ db -> getUserGroupsWithSummary db user contactId_ search_ )
2023-08-01 20:54:51 +01:00
ListGroups cName_ search_ -> withUser $ \ user @ User { userId } -> do
ct_ <- forM cName_ $ \ cName -> withStore $ \ db -> getContactByName db user cName
processChatCommand $ APIListGroups userId ( contactId' <$> ct_ ) search_
2022-07-29 19:04:32 +01:00
APIUpdateGroupProfile groupId p' -> withUser $ \ user -> do
2022-11-27 13:54:34 +00:00
g <- withStore $ \ db -> getGroup db user groupId
runUpdateGroupProfile user g p'
2022-12-10 08:27:32 +00:00
UpdateGroupNames gName GroupProfile { displayName , fullName } ->
updateGroupProfileByName gName $ \ p -> p { displayName , fullName }
2023-01-18 10:20:55 +00:00
ShowGroupProfile gName -> withUser $ \ user ->
CRGroupProfile user <$> withStore ( \ db -> getGroupInfoByName db user gName )
2022-12-10 08:27:32 +00:00
UpdateGroupDescription gName description ->
updateGroupProfileByName gName $ \ p -> p { description }
2023-08-01 20:54:51 +01:00
ShowGroupDescription gName -> withUser $ \ user ->
CRGroupDescription user <$> withStore ( \ db -> getGroupInfoByName db user gName )
2023-03-06 09:51:42 +00:00
APICreateGroupLink groupId mRole -> withUser $ \ user -> withChatLock " createGroupLink " $ do
2023-02-01 13:57:39 +00:00
gInfo <- withStore $ \ db -> getGroupInfo db user groupId
assertUserGroupRole gInfo GRAdmin
2023-03-06 09:51:42 +00:00
when ( mRole > GRMember ) $ throwChatError $ CEGroupMemberInitialRole gInfo mRole
2023-03-22 15:58:01 +00:00
groupLinkId <- GroupLinkId <$> drgRandomBytes 16
2023-09-10 22:40:15 +03:00
subMode <- chatReadVar subscriptionMode
2022-11-05 15:04:39 +04:00
let crClientData = encodeJSON $ CRDataGroup groupLinkId
2023-09-10 22:40:15 +03:00
( connId , cReq ) <- withAgent $ \ a -> createConnection a ( aUserId user ) True SCMContact ( Just crClientData ) subMode
withStore $ \ db -> createGroupLink db user gInfo connId cReq groupLinkId mRole subMode
2023-03-06 09:51:42 +00:00
pure $ CRGroupLinkCreated user gInfo cReq mRole
APIGroupLinkMemberRole groupId mRole' -> withUser $ \ user -> withChatLock " groupLinkMemberRole " $ do
gInfo <- withStore $ \ db -> getGroupInfo db user groupId
( groupLinkId , groupLink , mRole ) <- withStore $ \ db -> getGroupLink db user gInfo
assertUserGroupRole gInfo GRAdmin
when ( mRole' > GRMember ) $ throwChatError $ CEGroupMemberInitialRole gInfo mRole'
when ( mRole' /= mRole ) $ withStore' $ \ db -> setGroupLinkMemberRole db user groupLinkId mRole'
pure $ CRGroupLink user gInfo groupLink mRole'
2022-10-22 21:22:44 +01:00
APIDeleteGroupLink groupId -> withUser $ \ user -> withChatLock " deleteGroupLink " $ do
2022-10-13 17:12:22 +04:00
gInfo <- withStore $ \ db -> getGroupInfo db user groupId
deleteGroupLink' user gInfo
2023-01-04 21:06:28 +04:00
pure $ CRGroupLinkDeleted user gInfo
2022-10-13 17:12:22 +04:00
APIGetGroupLink groupId -> withUser $ \ user -> do
gInfo <- withStore $ \ db -> getGroupInfo db user groupId
2023-03-06 09:51:42 +00:00
( _ , groupLink , mRole ) <- withStore $ \ db -> getGroupLink db user gInfo
pure $ CRGroupLink user gInfo groupLink mRole
2023-09-16 17:55:48 +04:00
APICreateMemberContact gId gMemberId -> withUser $ \ user -> do
( g , m ) <- withStore $ \ db -> ( , ) <$> getGroupInfo db user gId <*> getGroupMember db user gId gMemberId
assertUserGroupRole g GRAuthor
unless ( groupFeatureAllowed SGFDirectMessages g ) $ throwChatError $ CECommandError " direct messages not allowed "
case memberConn m of
Just mConn @ Connection { peerChatVRange } -> do
unless ( isCompatibleRange ( fromJVersionRange peerChatVRange ) xGrpDirectInvVRange ) $ throwChatError CEPeerChatVRangeIncompatible
when ( isJust $ memberContactId m ) $ throwChatError $ CECommandError " member contact already exists "
subMode <- chatReadVar subscriptionMode
( connId , cReq ) <- withAgent $ \ a -> createConnection a ( aUserId user ) True SCMInvitation Nothing subMode
-- [incognito] reuse membership incognito profile
ct <- withStore' $ \ db -> createMemberContact db user connId cReq g m mConn subMode
pure $ CRNewMemberContact user ct g m
_ -> throwChatError CEGroupMemberNotActive
APISendMemberContactInvitation contactId msgContent_ -> withUser $ \ user -> do
( g , m , ct , cReq ) <- withStore $ \ db -> getMemberContact db user contactId
when ( contactGrpInvSent ct ) $ throwChatError $ CECommandError " x.grp.direct.inv already sent "
case memberConn m of
Just mConn -> do
let msg = XGrpDirectInv cReq msgContent_
2023-09-18 21:52:51 +01:00
( sndMsg , _ ) <- sendDirectMessage mConn msg ( GroupId $ g . groupId )
2023-09-16 17:55:48 +04:00
withStore' $ \ db -> setContactGrpInvSent db ct True
let ct' = ct { contactGrpInvSent = True }
forM_ msgContent_ $ \ mc -> do
ci <- saveSndChatItem user ( CDDirectSnd ct' ) sndMsg ( CISndMsgContent mc )
toView $ CRNewChatItem user ( AChatItem SCTDirect SMDSnd ( DirectChat ct' ) ci )
pure $ CRNewMemberContactSentInv user ct' g m
_ -> throwChatError CEGroupMemberNotActive
2023-03-06 09:51:42 +00:00
CreateGroupLink gName mRole -> withUser $ \ user -> do
groupId <- withStore $ \ db -> getGroupIdByName db user gName
processChatCommand $ APICreateGroupLink groupId mRole
GroupLinkMemberRole gName mRole -> withUser $ \ user -> do
2022-10-13 17:12:22 +04:00
groupId <- withStore $ \ db -> getGroupIdByName db user gName
2023-03-06 09:51:42 +00:00
processChatCommand $ APIGroupLinkMemberRole groupId mRole
2022-10-13 17:12:22 +04:00
DeleteGroupLink gName -> withUser $ \ user -> do
groupId <- withStore $ \ db -> getGroupIdByName db user gName
processChatCommand $ APIDeleteGroupLink groupId
ShowGroupLink gName -> withUser $ \ user -> do
groupId <- withStore $ \ db -> getGroupIdByName db user gName
processChatCommand $ APIGetGroupLink groupId
2022-03-13 19:34:03 +00:00
SendGroupMessageQuote gName cName quotedMsg msg -> withUser $ \ user -> do
2022-06-18 20:06:13 +01:00
groupId <- withStore $ \ db -> getGroupIdByName db user gName
2023-02-18 15:16:50 +00:00
quotedItemId <- withStore $ \ db -> getGroupChatItemIdByText db user groupId cName quotedMsg
let mc = MCText msg
2023-05-11 16:00:01 +04:00
processChatCommand . APISendMessage ( ChatRef CTGroup groupId ) False Nothing $ ComposedMessage Nothing ( Just quotedItemId ) mc
2023-01-16 12:10:47 +00:00
LastChats count_ -> withUser' $ \ user -> do
chats <- withStore' $ \ db -> getChatPreviews db user False
pure $ CRChats $ maybe id take count_ chats
2022-11-14 08:42:54 +00:00
LastMessages ( Just chatName ) count search -> withUser $ \ user -> do
2022-04-28 08:34:21 +01:00
chatRef <- getChatRef user chatName
2023-01-04 21:06:28 +04:00
chatResp <- processChatCommand $ APIGetChat chatRef ( CPLast count ) search
2023-06-04 23:38:25 -07:00
setActive $ chatActiveTo chatName
2023-01-04 21:06:28 +04:00
pure $ CRChatItems user ( aChatItems . chat $ chatResp )
LastMessages Nothing count search -> withUser $ \ user -> do
chatItems <- withStore $ \ db -> getAllChatItems db user ( CPLast count ) search
pure $ CRChatItems user chatItems
2022-12-17 15:33:58 +00:00
LastChatItemId ( Just chatName ) index -> withUser $ \ user -> do
chatRef <- getChatRef user chatName
2023-01-04 21:06:28 +04:00
chatResp <- processChatCommand ( APIGetChat chatRef ( CPLast $ index + 1 ) Nothing )
pure $ CRChatItemId user ( fmap aChatItemId . listToMaybe . aChatItems . chat $ chatResp )
LastChatItemId Nothing index -> withUser $ \ user -> do
chatItems <- withStore $ \ db -> getAllChatItems db user ( CPLast $ index + 1 ) Nothing
pure $ CRChatItemId user ( fmap aChatItemId . listToMaybe $ chatItems )
ShowChatItem ( Just itemId ) -> withUser $ \ user -> do
2023-05-18 17:52:58 +02:00
chatItem <- withStore $ \ db -> do
chatRef <- getChatRefViaItemId db user itemId
getAChatItem db user chatRef itemId
2023-01-04 21:06:28 +04:00
pure $ CRChatItems user ( ( : [] ) chatItem )
ShowChatItem Nothing -> withUser $ \ user -> do
chatItems <- withStore $ \ db -> getAllChatItems db user ( CPLast 1 ) Nothing
pure $ CRChatItems user chatItems
2023-05-08 20:07:51 +04:00
ShowChatItemInfo chatName msg -> withUser $ \ user -> do
chatRef <- getChatRef user chatName
itemId <- getChatItemIdByText user chatRef msg
2023-05-18 17:52:58 +02:00
processChatCommand $ APIGetChatItemInfo chatRef itemId
2023-01-18 10:20:55 +00:00
ShowLiveItems on -> withUser $ \ _ ->
asks showLiveItems >>= atomically . ( ` writeTVar ` on ) >> ok_
2022-04-30 19:18:46 +04:00
SendFile chatName f -> withUser $ \ user -> do
chatRef <- getChatRef user chatName
2023-09-01 19:43:27 +01:00
processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage ( Just $ CF . plain f ) Nothing ( MCFile " " )
2022-05-21 18:17:15 +04:00
SendImage chatName f -> withUser $ \ user -> do
chatRef <- getChatRef user chatName
filePath <- toFSFilePath f
2023-09-01 19:43:27 +01:00
unless ( any ( ` isSuffixOf ` map toLower f ) imageExtensions ) $ throwChatError CEFileImageType { filePath }
2022-05-21 18:17:15 +04:00
fileSize <- getFileSize filePath
unless ( fileSize <= maxImageSize ) $ throwChatError CEFileImageSize { filePath }
2023-03-09 11:01:22 +00:00
-- TODO include file description for preview
2023-09-01 19:43:27 +01:00
processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage ( Just $ CF . plain f ) Nothing ( MCImage " " fixedImagePreview )
2022-05-21 18:17:15 +04:00
ForwardFile chatName fileId -> forwardFile chatName fileId SendFile
ForwardImage chatName fileId -> forwardFile chatName fileId SendImage
2023-03-09 11:01:22 +00:00
SendFileDescription _chatName _f -> pure $ chatCmdError Nothing " TODO "
2023-09-01 19:43:27 +01:00
ReceiveFile fileId encrypted rcvInline_ filePath_ -> withUser $ \ _ ->
2022-10-22 21:22:44 +01:00
withChatLock " receiveFile " . procCmd $ do
2023-09-01 19:43:27 +01:00
( user , ft ) <- withStore ( ` getRcvFileTransferById ` fileId )
ft' <- if encrypted then encryptLocalFile ft else pure ft
receiveFile' user ft' rcvInline_ filePath_
where
encryptLocalFile ft @ RcvFileTransfer { xftpRcvFile } = case xftpRcvFile of
Nothing -> throwChatError $ CEFileInternal " locally encrypted files can't be received via SMP "
Just f -> do
cfArgs <- liftIO $ CF . randomArgs
withStore' $ \ db -> setFileCryptoArgs db fileId cfArgs
pure ft { xftpRcvFile = Just ( ( f :: XFTPRcvFile ) { cryptoArgs = Just cfArgs } ) }
SetFileToReceive fileId encrypted -> withUser $ \ _ -> do
2023-04-20 16:52:55 +04:00
withChatLock " setFileToReceive " . procCmd $ do
2023-09-01 19:43:27 +01:00
cfArgs <- if encrypted then fileCryptoArgs else pure Nothing
withStore' $ \ db -> setRcvFileToReceive db fileId cfArgs
2023-04-20 16:52:55 +04:00
ok_
2023-09-01 19:43:27 +01:00
where
fileCryptoArgs = do
( _ , RcvFileTransfer { xftpRcvFile = f } ) <- withStore ( ` getRcvFileTransferById ` fileId )
unless ( isJust f ) $ throwChatError $ CEFileInternal " locally encrypted files can't be received via SMP "
liftIO $ Just <$> CF . randomArgs
2022-05-11 16:18:28 +04:00
CancelFile fileId -> withUser $ \ user @ User { userId } ->
2022-10-22 21:22:44 +01:00
withChatLock " cancelFile " . procCmd $
2022-06-18 20:06:13 +01:00
withStore ( \ db -> getFileTransfer db user fileId ) >>= \ case
2023-04-18 12:48:36 +04:00
FTSnd ftm @ FileTransferMeta { xftpSndFile , cancelled } fts
2023-03-30 14:10:13 +04:00
| cancelled -> throwChatError $ CEFileCancel fileId " file already cancelled "
2023-04-18 12:48:36 +04:00
| not ( null fts ) && all fileCancelledOrCompleteSMP fts ->
2023-03-30 14:10:13 +04:00
throwChatError $ CEFileCancel fileId " file transfer is complete "
2023-03-29 17:18:44 +04:00
| otherwise -> do
2023-01-24 16:24:34 +04:00
fileAgentConnIds <- cancelSndFile user ftm fts True
deleteAgentConnectionsAsync user fileAgentConnIds
2022-06-18 20:06:13 +01:00
sharedMsgId <- withStore $ \ db -> getSharedMsgIdByFileId db userId fileId
2022-10-14 13:06:33 +01:00
withStore ( \ db -> getChatRefByFileId db user fileId ) >>= \ case
ChatRef CTDirect contactId -> do
2022-11-15 10:31:44 +04:00
contact <- withStore $ \ db -> getContact db user contactId
2022-10-14 13:06:33 +01:00
void . sendDirectContactMessage contact $ XFileCancel sharedMsgId
ChatRef CTGroup groupId -> do
Group gInfo ms <- withStore $ \ db -> getGroup db user groupId
2023-01-13 14:19:21 +04:00
void . sendGroupMessage user gInfo ms $ XFileCancel sharedMsgId
2022-10-14 13:06:33 +01:00
_ -> throwChatError $ CEFileInternal " invalid chat ref for file transfer "
2023-03-29 17:18:44 +04:00
ci <- withStore $ \ db -> getChatItemByFileId db user fileId
pure $ CRSndFileCancelled user ci ftm fts
2023-04-18 12:48:36 +04:00
where
fileCancelledOrCompleteSMP SndFileTransfer { fileStatus = s } =
s == FSCancelled || ( s == FSComplete && isNothing xftpSndFile )
2023-04-03 16:31:18 +04:00
FTRcv ftr @ RcvFileTransfer { cancelled , fileStatus , xftpRcvFile }
2023-03-30 14:10:13 +04:00
| cancelled -> throwChatError $ CEFileCancel fileId " file already cancelled "
| rcvFileComplete fileStatus -> throwChatError $ CEFileCancel fileId " file transfer is complete "
2023-04-03 16:31:18 +04:00
| otherwise -> case xftpRcvFile of
Nothing -> do
cancelRcvFileTransfer user ftr >>= mapM_ ( deleteAgentConnectionAsync user )
ci <- withStore $ \ db -> getChatItemByFileId db user fileId
pure $ CRRcvFileCancelled user ci ftr
Just XFTPRcvFile { agentRcvFileId } -> do
forM_ ( liveRcvFileTransferPath ftr ) $ \ filePath -> do
fsFilePath <- toFSFilePath filePath
2023-07-09 23:24:38 +01:00
liftIO $ removeFile fsFilePath ` catchAll_ ` pure ()
2023-04-03 16:31:18 +04:00
forM_ agentRcvFileId $ \ ( AgentRcvFileId aFileId ) ->
2023-04-25 15:46:00 +04:00
withAgent ( ` xftpDeleteRcvFile ` aFileId )
2023-04-03 16:31:18 +04:00
ci <- withStore $ \ db -> do
liftIO $ do
updateCIFileStatus db user fileId CIFSRcvInvitation
updateRcvFileStatus db fileId FSNew
updateRcvFileAgentId db fileId Nothing
getChatItemByFileId db user fileId
pure $ CRRcvFileCancelled user ci ftr
2023-01-04 21:06:28 +04:00
FileStatus fileId -> withUser $ \ user -> do
2023-04-21 13:36:44 +04:00
ci @ ( AChatItem _ _ _ ChatItem { file } ) <- withStore $ \ db -> getChatItemByFileId db user fileId
case file of
Just CIFile { fileProtocol = FPXFTP } ->
pure $ CRFileTransferStatusXFTP user ci
_ -> do
fileStatus <- withStore $ \ db -> getFileTransferProgress db user fileId
pure $ CRFileTransferStatus user fileStatus
2023-01-04 21:06:28 +04:00
ShowProfile -> withUser $ \ user @ User { profile } -> pure $ CRUserProfile user ( fromLocalProfile profile )
2022-03-10 15:45:40 +04:00
UpdateProfile displayName fullName -> withUser $ \ user @ User { profile } -> do
2022-08-18 11:35:31 +04:00
let p = ( fromLocalProfile profile :: Profile ) { displayName = displayName , fullName = fullName }
2022-03-10 15:45:40 +04:00
updateProfile user p
UpdateProfileImage image -> withUser $ \ user @ User { profile } -> do
2022-08-18 11:35:31 +04:00
let p = ( fromLocalProfile profile :: Profile ) { image }
2022-03-10 15:45:40 +04:00
updateProfile user p
2023-06-17 10:34:04 +01:00
ShowProfileImage -> withUser $ \ user @ User { profile } -> pure $ CRUserProfileImage user $ fromLocalProfile profile
2022-12-14 08:30:24 +00:00
SetUserFeature ( ACF f ) allowed -> withUser $ \ user @ User { profile } -> do
2022-11-27 13:54:34 +00:00
let p = ( fromLocalProfile profile :: Profile ) { preferences = Just . setPreference f ( Just allowed ) $ preferences' user }
updateProfile user p
2022-12-14 08:30:24 +00:00
SetContactFeature ( ACF f ) cName allowed_ -> withUser $ \ user -> do
2022-11-27 13:54:34 +00:00
ct @ Contact { userPreferences } <- withStore $ \ db -> getContactByName db user cName
let prefs' = setPreference f allowed_ $ Just userPreferences
updateContactPrefs user ct prefs'
2022-12-14 08:30:24 +00:00
SetGroupFeature ( AGF f ) gName enabled ->
2022-12-10 08:27:32 +00:00
updateGroupProfileByName gName $ \ p ->
p { groupPreferences = Just . setGroupPreference f enabled $ groupPreferences p }
2022-12-17 14:49:03 +04:00
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 } } <- withStore $ \ db -> getContactByName db 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
2023-05-17 13:31:24 +04:00
let pref = uncurry TimedMessagesGroupPreference $ maybe ( FEOff , Just 86400 ) ( \ ttl -> ( FEOn , Just ttl ) ) ttl_
2022-12-17 14:49:03 +04:00
updateGroupProfileByName gName $ \ p ->
p { groupPreferences = Just . setGroupPreference' SGFTimedMessages pref $ groupPreferences p }
2021-07-27 08:08:05 +01:00
QuitChat -> liftIO exitSuccess
2023-03-27 18:34:48 +01:00
ShowVersion -> do
2023-04-14 13:03:41 +02:00
let versionInfo = coreVersionInfo $ ( simplexmqCommitQ )
2023-08-12 18:27:10 +01:00
chatMigrations <- map upMigration <$> withStore' ( Migrations . getCurrent . DB . conn )
2023-03-27 18:34:48 +01:00
agentMigrations <- withAgent getAgentMigrations
pure $ CRVersionInfo { versionInfo , chatMigrations , agentMigrations }
2022-10-22 21:22:44 +01:00
DebugLocks -> do
chatLockName <- atomically . tryReadTMVar =<< asks chatLock
agentLocks <- withAgent debugAgentLocks
pure CRDebugLocks { chatLockName , agentLocks }
2022-12-26 22:24:34 +00:00
GetAgentStats -> CRAgentStats . map stat <$> withAgent getAgentStats
where
stat ( AgentStatsKey { host , clientTs , cmd , res } , count ) =
map B . unpack [ host , clientTs , cmd , res , bshow count ]
2023-01-18 10:20:55 +00:00
ResetAgentStats -> withAgent resetAgentStats >> ok_
2023-08-25 14:10:40 +01:00
GetAgentSubs -> summary <$> withAgent getAgentSubscriptions
where
2023-09-04 23:19:24 +01:00
summary SubscriptionsInfo { activeSubscriptions , pendingSubscriptions , removedSubscriptions } =
CRAgentSubs
{ activeSubs = foldl' countSubs M . empty activeSubscriptions ,
pendingSubs = foldl' countSubs M . empty pendingSubscriptions ,
removedSubs = foldl' accSubErrors M . empty removedSubscriptions
}
2023-08-25 14:10:40 +01:00
where
2023-09-04 23:19:24 +01:00
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
2023-08-25 14:10:40 +01:00
GetAgentSubsDetails -> CRAgentSubsDetails <$> withAgent getAgentSubscriptions
2021-07-12 19:00:03 +01:00
where
2022-10-22 21:22:44 +01:00
withChatLock name action = asks chatLock >>= \ l -> withLock l name action
2022-02-04 08:02:48 +00:00
-- below code would make command responses asynchronous where they can be slow
-- in View.hs `r'` should be defined as `id` in this case
2022-02-16 16:48:28 +04:00
-- procCmd :: m ChatResponse -> m ChatResponse
2022-02-16 20:31:26 +00:00
-- procCmd action = do
-- ChatController {chatLock = l, smpAgent = a, outputQ = q, idsDrg = gVar} <- ask
-- corrId <- liftIO $ SMP.CorrId <$> randomBytes gVar 8
-- void . forkIO $
2022-10-22 21:22:44 +01:00
-- withAgentLock a . withLock l name $
2023-07-09 23:24:38 +01:00
-- (atomically . writeTBQueue q) . (Just corrId,) =<< (action `catchChatError` (pure . CRChatError))
2022-02-16 20:31:26 +00:00
-- pure $ CRCmdAccepted corrId
-- use function below to make commands "synchronous"
procCmd :: m ChatResponse -> m ChatResponse
procCmd = id
2023-01-18 10:20:55 +00:00
ok_ = pure $ CRCmdOk Nothing
ok = pure . CRCmdOk . Just
2022-04-28 08:34:21 +01:00
getChatRef :: User -> ChatName -> m ChatRef
2022-09-05 15:23:38 +01:00
getChatRef user ( ChatName cType name ) =
2022-04-28 08:34:21 +01:00
ChatRef cType <$> case cType of
2022-09-05 15:23:38 +01:00
CTDirect -> withStore $ \ db -> getContactIdByName db user name
2022-06-18 20:06:13 +01:00
CTGroup -> withStore $ \ db -> getGroupIdByName db user name
2022-04-28 08:34:21 +01:00
_ -> throwChatError $ CECommandError " not supported "
2022-06-06 16:23:47 +01:00
checkChatStopped :: m ChatResponse -> m ChatResponse
checkChatStopped a = asks agentAsync >>= readTVarIO >>= maybe a ( const $ throwChatError CEChatNotStopped )
setStoreChanged :: m ()
setStoreChanged = asks chatStoreChanged >>= atomically . ( ` writeTVar ` True )
2022-08-31 18:07:34 +01:00
withStoreChanged :: m () -> m ChatResponse
2023-01-18 10:20:55 +00:00
withStoreChanged a = checkChatStopped $ a >> setStoreChanged >> ok_
2022-08-31 18:07:34 +01:00
checkStoreNotChanged :: m ChatResponse -> m ChatResponse
checkStoreNotChanged = ifM ( asks chatStoreChanged >>= readTVarIO ) ( throwChatError CEChatStoreChanged )
2023-01-04 21:06:28 +04:00
withUserName :: UserName -> ( UserId -> ChatCommand ) -> m ChatResponse
withUserName uName cmd = withStore ( ` getUserIdByName ` uName ) >>= processChatCommand . cmd
2022-12-09 15:26:43 +00:00
withContactName :: ContactName -> ( ContactId -> ChatCommand ) -> m ChatResponse
withContactName cName cmd = withUser $ \ user ->
withStore ( \ db -> getContactIdByName db user cName ) >>= processChatCommand . cmd
withMemberName :: GroupName -> ContactName -> ( GroupId -> GroupMemberId -> ChatCommand ) -> m ChatResponse
withMemberName gName mName cmd = withUser $ \ user ->
getGroupAndMemberId user gName mName >>= processChatCommand . uncurry cmd
getConnectionCode :: ConnId -> m Text
getConnectionCode connId = verificationCode <$> withAgent ( ` getConnectionRatchetAdHash ` connId )
2022-12-10 12:09:45 +00:00
verifyConnectionCode :: User -> Connection -> Maybe Text -> m ChatResponse
verifyConnectionCode user conn @ Connection { connId } ( Just code ) = do
2022-12-09 15:26:43 +00:00
code' <- getConnectionCode $ aConnId conn
let verified = sameVerificationCode code code'
when verified . withStore' $ \ db -> setConnectionVerified db user connId $ Just code'
2023-01-04 21:06:28 +04:00
pure $ CRConnectionVerified user verified code'
2022-12-10 12:09:45 +00:00
verifyConnectionCode user conn @ Connection { connId } _ = do
code' <- getConnectionCode $ aConnId conn
withStore' $ \ db -> setConnectionVerified db user connId Nothing
2023-01-04 21:06:28 +04:00
pure $ CRConnectionVerified user False code'
2023-02-18 15:16:50 +00:00
getSentChatItemIdByText :: User -> ChatRef -> Text -> m Int64
2022-04-28 08:34:21 +01:00
getSentChatItemIdByText user @ User { userId , localDisplayName } ( ChatRef cType cId ) msg = case cType of
2023-02-18 15:16:50 +00:00
CTDirect -> withStore $ \ db -> getDirectChatItemIdByText db userId cId SMDSnd msg
CTGroup -> withStore $ \ db -> getGroupChatItemIdByText db user cId ( Just localDisplayName ) msg
2022-04-28 07:26:43 +01:00
_ -> throwChatError $ CECommandError " not supported "
2023-05-08 20:07:51 +04:00
getChatItemIdByText :: User -> ChatRef -> Text -> m Int64
getChatItemIdByText user ( ChatRef cType cId ) msg = case cType of
CTDirect -> withStore $ \ db -> getDirectChatItemIdByText' db user cId msg
CTGroup -> withStore $ \ db -> getGroupChatItemIdByText' db user cId msg
_ -> throwChatError $ CECommandError " not supported "
2023-08-08 17:25:28 +04:00
connectViaContact :: User -> IncognitoEnabled -> ConnectionRequestUri 'CMContact -> m ChatResponse
connectViaContact user @ User { userId } incognito cReq @ ( CRContactUri ConnReqUriData { crClientData } ) = withChatLock " connectViaContact " $ do
2022-02-13 13:19:24 +04:00
let cReqHash = ConnReqUriHash . C . sha256Hash $ strEncode cReq
2022-11-15 10:31:44 +04:00
withStore' ( \ db -> getConnReqContactXContactId db user cReqHash ) >>= \ case
2023-01-04 21:06:28 +04:00
( Just contact , _ ) -> pure $ CRContactAlreadyExists user contact
2022-02-13 13:19:24 +04:00
( _ , xContactId_ ) -> procCmd $ do
2023-03-22 15:58:01 +00:00
let randomXContactId = XContactId <$> drgRandomBytes 16
2022-02-13 13:19:24 +04:00
xContactId <- maybe randomXContactId pure xContactId_
2023-09-10 22:40:15 +03:00
subMode <- chatReadVar subscriptionMode
2022-08-18 11:35:31 +04:00
-- [incognito] generate profile to send
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
2022-11-11 18:34:32 +04:00
let profileToSend = userProfileToSend user incognitoProfile Nothing
2023-09-01 19:20:07 +04:00
dm <- directMessage ( XContact profileToSend $ Just xContactId )
2023-09-10 22:40:15 +03:00
connId <- withAgent $ \ a -> joinConnection a ( aUserId user ) True cReq dm subMode
2022-11-05 15:04:39 +04:00
let groupLinkId = crClientData >>= decodeJSON >>= \ ( CRDataGroup gli ) -> Just gli
2023-09-10 22:40:15 +03:00
conn <- withStore' $ \ db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile groupLinkId subMode
2023-01-04 21:06:28 +04:00
toView $ CRNewContactConnection user conn
pure $ CRSentInvitation user incognitoProfile
2021-08-02 20:10:24 +01:00
contactMember :: Contact -> [ GroupMember ] -> Maybe GroupMember
contactMember Contact { contactId } =
find $ \ GroupMember { memberContactId = cId , memberStatus = s } ->
cId == Just contactId && s /= GSMemRemoved && s /= GSMemLeft
2023-09-01 19:43:27 +01:00
checkSndFile :: MsgContent -> CryptoFile -> Integer -> m ( Integer , SendFileMode )
checkSndFile mc ( CryptoFile f cfArgs ) n = do
2022-04-15 09:36:38 +04:00
fsFilePath <- toFSFilePath f
unlessM ( doesFileExist fsFilePath ) . throwChatError $ CEFileNotFound f
2022-10-14 13:06:33 +01:00
ChatConfig { fileChunkSize , inlineFiles } <- asks config
2023-03-13 10:30:32 +00:00
xftpCfg <- readTVarIO =<< asks userXFTPFileConfig
2023-09-01 19:43:27 +01:00
fileSize <- liftIO $ CF . getFileContentsSize $ CryptoFile fsFilePath cfArgs
2023-04-21 13:46:56 +04:00
when ( fromInteger fileSize > maxFileSize ) $ throwChatError $ CEFileSize f
2022-10-15 14:48:07 +04:00
let chunks = - ( ( - fileSize ) ` div ` fileChunkSize )
2023-03-13 10:30:32 +00:00
fileInline = inlineFileMode mc inlineFiles chunks n
fileMode = case xftpCfg of
Just cfg
2023-09-01 19:43:27 +01:00
| isJust cfArgs -> SendFileXFTP
2023-04-17 15:33:15 +04:00
| fileInline == Just IFMSent || fileSize < minFileSize cfg || n <= 0 -> SendFileSMP fileInline
2023-03-14 11:42:44 +04:00
| otherwise -> SendFileXFTP
2023-03-13 10:30:32 +00:00
_ -> SendFileSMP fileInline
pure ( fileSize , fileMode )
2022-11-26 22:39:56 +00:00
inlineFileMode mc InlineFilesConfig { offerChunks , sendChunks , totalSendChunks } chunks n
2022-10-14 13:06:33 +01:00
| chunks > offerChunks = Nothing
2022-11-26 22:39:56 +00:00
| chunks <= sendChunks && chunks * n <= totalSendChunks && isVoice mc = Just IFMSent
| otherwise = Just IFMOffer
2022-03-10 15:45:40 +04:00
updateProfile :: User -> Profile -> m ChatResponse
2023-04-27 17:19:21 +04:00
updateProfile user p' = updateProfile_ user p' $ withStore $ \ db -> updateUserProfile db user p'
updateProfile_ :: User -> Profile -> m User -> m ChatResponse
updateProfile_ user @ User { profile = p } p' updateUser
2023-01-04 21:06:28 +04:00
| p' == fromLocalProfile p = pure $ CRUserProfileNoChange user
2022-03-29 08:53:30 +01:00
| otherwise = do
2022-12-21 19:54:44 +04:00
-- read contacts before user update to correctly merge preferences
2022-10-15 14:48:07 +04:00
-- [incognito] filter out contacts with whom user has incognito connections
contacts <-
filter ( \ ct -> isReady ct && not ( contactConnIncognito ct ) )
<$> withStore' ( ` getUserContacts ` user )
2023-04-27 17:19:21 +04:00
user' <- updateUser
2022-12-21 19:54:44 +04:00
asks currentUser >>= atomically . ( ` writeTVar ` Just user' )
2022-10-22 21:22:44 +01:00
withChatLock " updateProfile " . procCmd $ do
2023-06-17 10:34:04 +01:00
ChatConfig { logLevel } <- asks config
2023-08-22 16:13:57 +01:00
summary <- foldM ( processAndCount user' logLevel ) ( UserProfileUpdateSummary 0 0 0 [] ) contacts
pure $ CRUserProfileUpdated user' ( fromLocalProfile p ) p' summary
2023-01-12 16:31:27 +04:00
where
2023-09-01 19:43:27 +01:00
processAndCount user' ll s @ UserProfileUpdateSummary { notChanged , updateSuccesses , updateFailures , changedContacts = cts } ct = do
2023-01-12 16:31:27 +04:00
let mergedProfile = userProfileToSend user Nothing $ Just ct
ct' = updateMergedPreferences user' ct
mergedProfile' = userProfileToSend user' Nothing $ Just ct'
2023-08-22 16:13:57 +01:00
if mergedProfile' == mergedProfile
then pure s { notChanged = notChanged + 1 }
2023-09-05 20:15:50 +04:00
else
let cts' = if mergedPreferences ct == mergedPreferences ct' then cts else ct' : cts
2023-08-22 16:13:57 +01:00
in ( notifyContact mergedProfile' ct' $> s { updateSuccesses = updateSuccesses + 1 , changedContacts = cts' } )
` catchChatError ` \ e -> when ( ll <= CLLInfo ) ( toView $ CRChatError ( Just user ) e ) $> s { updateFailures = updateFailures + 1 , changedContacts = cts' }
2023-09-05 20:15:50 +04:00
where
2023-08-22 16:13:57 +01:00
notifyContact mergedProfile' ct' = do
void $ sendDirectContactMessage ct' ( XInfo mergedProfile' )
when ( directOrUsed ct' ) $ createSndFeatureItems user' ct ct'
2022-11-04 17:05:21 +00:00
updateContactPrefs :: User -> Contact -> Preferences -> m ChatResponse
2022-12-21 14:10:05 +04:00
updateContactPrefs user @ User { userId } ct @ Contact { activeConn = Connection { customUserProfileId } , userPreferences = contactUserPrefs } contactUserPrefs'
2023-01-04 21:06:28 +04:00
| contactUserPrefs == contactUserPrefs' = pure $ CRContactPrefsUpdated user ct ct
2022-11-01 17:32:49 +03:00
| otherwise = do
2022-12-03 18:06:21 +00:00
assertDirectAllowed user MDSnd ct XInfo_
2022-11-15 10:31:44 +04:00
ct' <- withStore' $ \ db -> updateContactUserPreferences db user ct contactUserPrefs'
2022-12-21 14:10:05 +04:00
incognitoProfile <- forM customUserProfileId $ \ profileId -> withStore $ \ db -> getProfileById db userId profileId
2022-12-21 19:54:44 +04:00
let mergedProfile = userProfileToSend user ( fromLocalProfile <$> incognitoProfile ) ( Just ct )
mergedProfile' = userProfileToSend user ( fromLocalProfile <$> incognitoProfile ) ( Just ct' )
when ( mergedProfile' /= mergedProfile ) $
withChatLock " updateProfile " $ do
2023-07-09 23:24:38 +01:00
void ( sendDirectContactMessage ct' $ XInfo mergedProfile' ) ` catchChatError ` ( toView . CRChatError ( Just user ) )
2022-12-22 14:56:29 +00:00
when ( directOrUsed ct' ) $ createSndFeatureItems user ct ct'
2023-01-04 21:06:28 +04:00
pure $ CRContactPrefsUpdated user ct ct'
2022-11-27 13:54:34 +00:00
runUpdateGroupProfile :: User -> Group -> GroupProfile -> m ChatResponse
runUpdateGroupProfile user ( Group g @ GroupInfo { groupProfile = p } ms ) p' = do
2023-02-01 13:57:39 +00:00
assertUserGroupRole g GROwner
2022-11-27 13:54:34 +00:00
g' <- withStore $ \ db -> updateGroupProfile db user g p'
2023-07-26 14:49:35 +04:00
( msg , _ ) <- sendGroupMessage user g' ms ( XGrpInfo p' )
2022-11-27 13:54:34 +00:00
let cd = CDGroupSnd g'
unless ( sameGroupProfileInfo p p' ) $ do
2022-12-16 07:51:04 +00:00
ci <- saveSndChatItem user cd msg ( CISndGroupEvent $ SGEGroupUpdated p' )
2023-01-04 21:06:28 +04:00
toView $ CRNewChatItem user ( AChatItem SCTGroup SMDSnd ( GroupChat g' ) ci )
2022-12-19 21:18:59 +04:00
createGroupFeatureChangedItems user cd CISndGroupFeature g g'
2023-01-04 21:06:28 +04:00
pure $ CRGroupUpdated user g g' Nothing
2023-02-01 13:57:39 +00:00
assertUserGroupRole :: GroupInfo -> GroupMemberRole -> m ()
assertUserGroupRole g @ GroupInfo { membership } requiredRole = do
2023-08-25 04:56:37 +08:00
when ( membership . memberRole < requiredRole ) $ throwChatError $ CEGroupUserRole g requiredRole
2023-02-01 13:57:39 +00:00
when ( memberStatus membership == GSMemInvited ) $ throwChatError ( CEGroupNotJoined g )
when ( memberRemoved membership ) $ throwChatError CEGroupMemberUserRemoved
unless ( memberActive membership ) $ throwChatError CEGroupMemberNotActive
2023-02-08 22:29:36 +04:00
delGroupChatItem :: User -> GroupInfo -> CChatItem 'CTGroup -> MessageId -> Maybe GroupMember -> m ChatResponse
delGroupChatItem user gInfo @ GroupInfo { localDisplayName = gName } ci msgId byGroupMember = do
2023-02-08 07:08:53 +00:00
setActive $ ActiveG gName
2023-05-19 14:52:51 +02:00
deletedTs <- liftIO getCurrentTime
2023-02-08 07:08:53 +00:00
if groupFeatureAllowed SGFFullDelete gInfo
2023-05-19 14:52:51 +02:00
then deleteGroupCI user gInfo ci True False byGroupMember deletedTs
else markGroupCIDeleted user gInfo ci msgId True byGroupMember deletedTs
2022-12-10 08:27:32 +00:00
updateGroupProfileByName :: GroupName -> ( GroupProfile -> GroupProfile ) -> m ChatResponse
updateGroupProfileByName gName update = withUser $ \ user -> do
g @ ( Group GroupInfo { groupProfile = p } _ ) <- withStore $ \ db ->
getGroupIdByName db user gName >>= getGroup db user
runUpdateGroupProfile user g $ update p
2022-03-29 08:53:30 +01:00
isReady :: Contact -> Bool
isReady ct =
2023-08-25 04:56:37 +08:00
let s = connStatus $ ct . activeConn
2022-03-29 08:53:30 +01:00
in s == ConnReady || s == ConnSndReady
2022-12-20 12:58:15 +00:00
withCurrentCall :: ContactId -> ( User -> Contact -> Call -> m ( Maybe Call ) ) -> m ChatResponse
2023-01-16 15:06:03 +04:00
withCurrentCall ctId action = do
( user , ct ) <- withStore $ \ db -> do
user <- getUserByContactId db ctId
( user , ) <$> getContact db user ctId
2022-05-04 13:31:00 +01:00
calls <- asks currentCalls
2022-10-22 21:22:44 +01:00
withChatLock " currentCall " $
2022-05-04 13:31:00 +01:00
atomically ( TM . lookup ctId calls ) >>= \ case
2022-05-03 10:22:35 +01:00
Nothing -> throwChatError CENoCurrentCall
Just call @ Call { contactId }
| ctId == contactId -> do
2022-12-20 12:58:15 +00:00
call_ <- action user ct call
2022-10-15 14:48:07 +04:00
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
2023-01-18 10:20:55 +00:00
ok user
2022-05-03 10:22:35 +01:00
| otherwise -> throwChatError $ CECallContact contactId
2023-04-05 21:59:12 +01:00
withServerProtocol :: ProtocolTypeI p => SProtocolType p -> ( UserProtocol p => m a ) -> m a
withServerProtocol p action = case userProtocol p of
Just Dict -> action
_ -> throwChatError $ CEServerProtocol $ AProtocolType p
2022-05-21 18:17:15 +04:00
forwardFile :: ChatName -> FileTransferId -> ( ChatName -> FilePath -> ChatCommand ) -> m ChatResponse
forwardFile chatName fileId sendCommand = withUser $ \ user -> do
2022-06-18 20:06:13 +01:00
withStore ( \ db -> getFileTransfer db user fileId ) >>= \ case
2022-05-21 18:17:15 +04:00
FTRcv RcvFileTransfer { fileStatus = RFSComplete RcvFileInfo { filePath } } -> forward filePath
FTSnd { fileTransferMeta = FileTransferMeta { filePath } } -> forward filePath
_ -> throwChatError CEFileNotReceived { fileId }
where
forward = processChatCommand . sendCommand chatName
2022-12-09 15:26:43 +00:00
getGroupAndMemberId :: User -> GroupName -> ContactName -> m ( GroupId , GroupMemberId )
getGroupAndMemberId user gName groupMemberName =
2022-07-12 19:20:56 +04:00
withStore $ \ db -> do
groupId <- getGroupIdByName db user gName
groupMemberId <- getGroupMemberIdByName db user groupId groupMemberName
pure ( groupId , groupMemberId )
2022-10-21 17:35:07 +04:00
sendGrpInvitation :: User -> Contact -> GroupInfo -> GroupMember -> ConnReqInvitation -> m ()
sendGrpInvitation user ct @ Contact { localDisplayName } GroupInfo { groupId , groupProfile , membership } GroupMember { groupMemberId , memberId , memberRole = memRole } cReq = do
let GroupMember { memberRole = userRole , memberId = userMemberId } = membership
2022-11-03 14:46:36 +04:00
groupInv = GroupInvitation ( MemberIdRole userMemberId userRole ) ( MemberIdRole memberId memRole ) cReq groupProfile Nothing
2022-10-21 17:35:07 +04:00
( msg , _ ) <- sendDirectContactMessage ct $ XGrpInv groupInv
let content = CISndGroupInvitation ( CIGroupInvitation { groupId , groupMemberId , localDisplayName , groupProfile , status = CIGISPending } ) memRole
2022-12-16 07:51:04 +00:00
ci <- saveSndChatItem user ( CDDirectSnd ct ) msg content
2023-01-04 21:06:28 +04:00
toView $ CRNewChatItem user ( AChatItem SCTDirect SMDSnd ( DirectChat ct ) ci )
2022-10-21 17:35:07 +04:00
setActive $ ActiveG localDisplayName
2023-05-11 16:00:01 +04:00
sndContactCITimed :: Bool -> Contact -> Maybe Int -> m ( Maybe CITimed )
sndContactCITimed live = sndCITimed_ live . contactTimedTTL
sndGroupCITimed :: Bool -> GroupInfo -> Maybe Int -> m ( Maybe CITimed )
sndGroupCITimed live = sndCITimed_ live . groupTimedTTL
sndCITimed_ :: Bool -> Maybe ( Maybe Int ) -> Maybe Int -> m ( Maybe CITimed )
sndCITimed_ live chatTTL itemTTL =
forM ( chatTTL >>= ( itemTTL <|> ) ) $ \ ttl ->
CITimed ttl
<$> if live
2023-05-15 21:07:03 +04:00
then pure Nothing
else Just . addUTCTime ( realToFrac ttl ) <$> liftIO getCurrentTime
2023-03-22 15:58:01 +00:00
drgRandomBytes :: Int -> m ByteString
drgRandomBytes n = asks idsDrg >>= liftIO . ( ` randomBytes ` n )
privateGetUser :: UserId -> m User
privateGetUser userId =
2023-08-12 18:27:10 +01:00
tryChatError ( withStore ( ` getUser ` userId ) ) >>= \ case
2023-03-22 15:58:01 +00:00
Left _ -> throwChatError CEUserUnknown
Right user -> pure user
validateUserPassword :: User -> User -> Maybe UserPwd -> m ()
validateUserPassword User { userId } User { userId = userId' , viewPwdHash } viewPwd_ =
forM_ viewPwdHash $ \ pwdHash ->
let pwdOk = case viewPwd_ of
Nothing -> userId == 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 )
2023-03-29 19:28:06 +01:00
setUserNotifications :: UserId -> Bool -> m ChatResponse
setUserNotifications userId' showNtfs = withUser $ \ user -> do
user' <- privateGetUser userId'
case viewPwdHash user' of
Just _ -> throwChatError $ CEHiddenUserAlwaysMuted userId'
_ -> setUserPrivacy user user' { showNtfs }
2023-03-29 17:39:04 +01:00
setUserPrivacy :: User -> User -> m ChatResponse
setUserPrivacy user @ User { userId } user' @ User { userId = userId' }
| userId == userId' = do
asks currentUser >>= atomically . ( ` writeTVar ` Just user' )
withStore' ( ` updateUserPrivacy ` user' )
pure $ CRUserPrivacy { user = user' , updatedUser = user' }
| otherwise = do
withStore' ( ` updateUserPrivacy ` user' )
pure $ CRUserPrivacy { user , updatedUser = user' }
2023-03-22 15:58:01 +00:00
checkDeleteChatUser :: User -> m ()
checkDeleteChatUser user @ User { userId } = do
when ( activeUser user ) $ throwChatError ( CECantDeleteActiveUser userId )
users <- withStore' getUsers
unless ( length users > 1 && ( isJust ( viewPwdHash user ) || length ( filter ( isNothing . viewPwdHash ) users ) > 1 ) ) $
throwChatError ( CECantDeleteLastUser userId )
setActive ActiveNone
deleteChatUser :: User -> Bool -> m ChatResponse
deleteChatUser user delSMPQueues = do
filesInfo <- withStore' ( ` getUserFileInfo ` user )
forM_ filesInfo $ \ fileInfo -> deleteFile user fileInfo
withAgent $ \ a -> deleteUser a ( aUserId user ) delSMPQueues
withStore' ( ` deleteUserRecord ` user )
ok_
2023-07-13 23:48:25 +01:00
updateChatSettings :: ChatName -> ( ChatSettings -> ChatSettings ) -> m ChatResponse
updateChatSettings ( ChatName cType name ) updateSettings = withUser $ \ user -> do
( chatId , chatSettings ) <- case cType of
CTDirect -> withStore $ \ db -> do
ctId <- getContactIdByName db user name
Contact { chatSettings } <- getContact db user ctId
pure ( ctId , chatSettings )
CTGroup -> withStore $ \ db -> do
gId <- getGroupIdByName db user name
GroupInfo { chatSettings } <- getGroupInfo db user gId
pure ( gId , chatSettings )
_ -> throwChatError $ CECommandError " not supported "
processChatCommand $ APISetChatSettings ( ChatRef cType chatId ) $ updateSettings chatSettings
2022-04-10 13:30:58 +04:00
2022-12-03 18:06:21 +00:00
assertDirectAllowed :: ChatMonad m => User -> MsgDirection -> Contact -> CMEventTag e -> m ()
assertDirectAllowed user dir ct event =
2022-12-12 15:27:52 +04:00
unless ( allowedChatEvent || anyDirectOrUsed ct ) . unlessM directMessagesAllowed $
2022-12-03 18:06:21 +00:00
throwChatError $ CEDirectMessagesProhibited dir ct
where
2022-12-14 08:30:24 +00:00
directMessagesAllowed = any ( groupFeatureAllowed' SGFDirectMessages ) <$> withStore' ( \ db -> getContactGroupPreferences db user ct )
2022-12-03 18:06:21 +00:00
allowedChatEvent = case event of
XMsgNew_ -> False
XMsgUpdate_ -> False
XMsgDel_ -> False
XFile_ -> False
XGrpInv_ -> False
XCallInv_ -> False
_ -> True
2023-04-12 14:47:54 +04:00
roundedFDCount :: Int -> Int
2023-04-17 15:33:15 +04:00
roundedFDCount n
| n <= 0 = 4
| otherwise = max 4 $ fromIntegral $ ( 2 :: Integer ) ^ ( ceiling ( logBase 2 ( fromIntegral n ) :: Double ) :: Integer )
2023-04-12 14:47:54 +04:00
2023-03-22 15:58:01 +00:00
startExpireCIThread :: forall m . ChatMonad' m => User -> m ()
2023-01-14 17:52:40 +04:00
startExpireCIThread user @ User { userId } = do
expireThreads <- asks expireCIThreads
atomically ( TM . lookup userId expireThreads ) >>= \ case
Nothing -> do
a <- Just <$> async ( void $ runExceptT runExpireCIs )
atomically $ TM . insert userId a expireThreads
_ -> pure ()
where
2023-01-25 19:29:09 +04:00
runExpireCIs = do
2023-05-29 15:18:22 +04:00
delay <- asks ( initialCleanupManagerDelay . config )
liftIO $ threadDelay' delay
2023-01-25 19:29:09 +04:00
interval <- asks $ ciExpirationInterval . config
forever $ do
2023-07-09 23:24:38 +01:00
flip catchChatError ( toView . CRChatError ( Just user ) ) $ do
2023-01-25 19:29:09 +04:00
expireFlags <- asks expireCIFlags
atomically $ TM . lookup userId expireFlags >>= \ b -> unless ( b == Just True ) retry
2023-05-29 15:18:22 +04:00
ttl <- withStoreCtx' ( Just " startExpireCIThread, getChatItemTTL " ) ( ` getChatItemTTL ` user )
2023-01-25 19:29:09 +04:00
forM_ ttl $ \ t -> expireChatItems user t False
2023-04-04 14:26:31 +01:00
liftIO $ threadDelay' interval
2023-01-14 17:52:40 +04:00
2023-03-22 15:58:01 +00:00
setExpireCIFlag :: ChatMonad' m => User -> Bool -> m ()
2023-01-13 21:01:26 +04:00
setExpireCIFlag User { userId } b = do
expireFlags <- asks expireCIFlags
atomically $ TM . insert userId b expireFlags
2023-03-22 15:58:01 +00:00
setAllExpireCIFlags :: ChatMonad' m => Bool -> m ()
2023-01-13 21:01:26 +04:00
setAllExpireCIFlags b = do
expireFlags <- asks expireCIFlags
atomically $ do
keys <- M . keys <$> readTVar expireFlags
forM_ keys $ \ k -> TM . insert k b expireFlags
2022-09-28 20:47:06 +04:00
2023-07-09 23:24:38 +01:00
deleteFilesAndConns :: ChatMonad m => User -> [ CIFileInfo ] -> m ()
2023-01-24 16:24:34 +04:00
deleteFilesAndConns user filesInfo = do
connIds <- mapM ( deleteFile user ) filesInfo
deleteAgentConnectionsAsync user $ concat connIds
2023-07-09 23:24:38 +01:00
deleteFile :: ChatMonad m => User -> CIFileInfo -> m [ ConnId ]
2023-01-18 17:08:48 +04:00
deleteFile user fileInfo = deleteFile' user fileInfo False
2023-01-24 16:24:34 +04:00
deleteFile' :: forall m . ChatMonad m => User -> CIFileInfo -> Bool -> m [ ConnId ]
2023-04-03 18:49:22 +04:00
deleteFile' user ciFileInfo @ CIFileInfo { filePath } sendCancel = do
aConnIds <- cancelFile' user ciFileInfo sendCancel
2023-07-09 23:24:38 +01:00
delete ` catchChatError ` ( toView . CRChatError ( Just user ) )
2023-01-24 16:24:34 +04:00
pure aConnIds
2023-04-03 18:49:22 +04:00
where
delete :: m ()
delete = withFilesFolder $ \ filesFolder ->
2023-07-09 23:24:38 +01:00
liftIO . forM_ filePath $ \ fPath -> do
2023-04-03 18:49:22 +04:00
let fsFilePath = filesFolder </> fPath
2023-07-09 23:24:38 +01:00
removeFile fsFilePath ` catchAll ` \ _ ->
removePathForcibly fsFilePath ` catchAll_ ` pure ()
2023-04-03 18:49:22 +04:00
-- perform an action only if filesFolder is set (i.e. on mobile devices)
withFilesFolder :: ( FilePath -> m () ) -> m ()
withFilesFolder action = asks filesFolder >>= readTVarIO >>= mapM_ action
cancelFile' :: forall m . ChatMonad m => User -> CIFileInfo -> Bool -> m [ ConnId ]
cancelFile' user CIFileInfo { fileId , fileStatus } sendCancel =
case fileStatus of
2023-07-09 23:24:38 +01:00
Just fStatus -> cancel' fStatus ` catchChatError ` ( \ e -> toView ( CRChatError ( Just user ) e ) $> [] )
2023-04-03 18:49:22 +04:00
Nothing -> pure []
2022-10-04 01:33:36 +04:00
where
2023-01-24 16:24:34 +04:00
cancel' :: ACIFileStatus -> m [ ConnId ]
cancel' ( AFS dir status ) =
if ciFileEnded status
then pure []
else case dir of
2022-10-05 19:54:28 +04:00
SMDSnd -> do
( ftm @ FileTransferMeta { cancelled } , fts ) <- withStore ( \ db -> getSndFileTransfer db user fileId )
2023-01-24 16:24:34 +04:00
if cancelled then pure [] else cancelSndFile user ftm fts sendCancel
2022-10-05 19:54:28 +04:00
SMDRcv -> do
ft @ RcvFileTransfer { cancelled } <- withStore ( \ db -> getRcvFileTransfer db user fileId )
2023-01-24 16:24:34 +04:00
if cancelled then pure [] else maybeToList <$> cancelRcvFileTransfer user ft
2022-09-28 20:47:06 +04:00
2022-12-20 12:58:15 +00:00
updateCallItemStatus :: ChatMonad m => User -> Contact -> Call -> WebRTCCallStatus -> Maybe MessageId -> m ()
updateCallItemStatus user ct Call { chatItemId } receivedStatus msgId_ = do
aciContent_ <- callStatusItemContent user ct chatItemId receivedStatus
forM_ aciContent_ $ \ aciContent -> updateDirectChatItemView user ct chatItemId aciContent False msgId_
2022-05-04 13:31:00 +01:00
2022-12-20 12:58:15 +00:00
updateDirectChatItemView :: ChatMonad m => User -> Contact -> ChatItemId -> ACIContent -> Bool -> Maybe MessageId -> m ()
updateDirectChatItemView user ct @ Contact { contactId } chatItemId ( ACIContent msgDir ciContent ) live msgId_ = do
ci' <- withStore $ \ db -> updateDirectChatItem db user contactId chatItemId ciContent live msgId_
2023-01-04 21:06:28 +04:00
toView $ CRChatItemUpdated user ( AChatItem SCTDirect msgDir ( DirectChat ct ) ci' )
2022-05-04 13:31:00 +01:00
2022-12-20 12:58:15 +00:00
callStatusItemContent :: ChatMonad m => User -> Contact -> ChatItemId -> WebRTCCallStatus -> m ( Maybe ACIContent )
callStatusItemContent user Contact { contactId } chatItemId receivedStatus = do
2022-05-04 13:31:00 +01:00
CChatItem msgDir ChatItem { meta = CIMeta { updatedAt } , content } <-
2022-12-20 12:58:15 +00:00
withStore $ \ db -> getDirectChatItem db user contactId chatItemId
2022-05-04 13:31:00 +01:00
ts <- liftIO getCurrentTime
let callDuration :: Int = nominalDiffTimeToSeconds ( ts ` diffUTCTime ` updatedAt ) ` div' ` 1
callStatus = case content of
CISndCall st _ -> Just st
CIRcvCall st _ -> Just st
_ -> Nothing
newState_ = case ( callStatus , receivedStatus ) of
( Just CISCallProgress , WCSConnected ) -> Nothing -- if call in-progress received connected -> no change
( Just CISCallProgress , WCSDisconnected ) -> Just ( CISCallEnded , callDuration ) -- calculate in-progress duration
( Just CISCallProgress , WCSFailed ) -> Just ( CISCallEnded , callDuration ) -- whether call disconnected or failed
2022-05-20 07:43:44 +01:00
( Just CISCallPending , WCSDisconnected ) -> Just ( CISCallMissed , 0 )
2022-05-04 13:31:00 +01:00
( Just CISCallEnded , _ ) -> Nothing -- if call already ended or failed -> no change
( Just CISCallError , _ ) -> Nothing
2022-05-24 19:34:27 +01:00
( Just _ , WCSConnecting ) -> Just ( CISCallNegotiated , 0 )
2022-05-04 13:31:00 +01:00
( Just _ , WCSConnected ) -> Just ( CISCallProgress , 0 ) -- if call ended that was never connected, duration = 0
( Just _ , WCSDisconnected ) -> Just ( CISCallEnded , 0 )
( Just _ , WCSFailed ) -> Just ( CISCallError , 0 )
( Nothing , _ ) -> Nothing -- some other content - we should never get here, but no exception is thrown
pure $ aciContent msgDir <$> newState_
where
aciContent :: forall d . SMsgDirection d -> ( CICallStatus , Int ) -> ACIContent
aciContent msgDir ( callStatus' , duration ) = case msgDir of
SMDSnd -> ACIContent SMDSnd $ CISndCall callStatus' duration
SMDRcv -> ACIContent SMDRcv $ CIRcvCall callStatus' duration
2022-04-15 09:36:38 +04:00
-- mobile clients use file paths relative to app directory (e.g. for the reason ios app directory changes on updates),
-- so we have to differentiate between the file path stored in db and communicated with frontend, and the file path
-- used during file transfer for actual operations with file system
2023-07-09 23:24:38 +01:00
toFSFilePath :: ChatMonad' m => FilePath -> m FilePath
2022-04-15 13:16:34 +01:00
toFSFilePath f =
2023-03-22 18:48:38 +04:00
maybe f ( </> f ) <$> ( readTVarIO =<< asks filesFolder )
2022-04-15 09:36:38 +04:00
2023-04-20 16:52:55 +04:00
receiveFile' :: ChatMonad m => User -> RcvFileTransfer -> Maybe Bool -> Maybe FilePath -> m ChatResponse
receiveFile' user ft rcvInline_ filePath_ = do
2023-07-09 23:24:38 +01:00
( CRRcvFileAccepted user <$> acceptFileReceive user ft rcvInline_ filePath_ ) ` catchChatError ` processError
2023-04-20 16:52:55 +04:00
where
processError = \ case
-- TODO AChatItem in Cancelled events
ChatErrorAgent ( SMP SMP . AUTH ) _ -> pure $ CRRcvFileAcceptedSndCancelled user ft
ChatErrorAgent ( CONN DUPLICATE ) _ -> pure $ CRRcvFileAcceptedSndCancelled user ft
e -> throwError e
2022-10-20 14:32:20 +01:00
acceptFileReceive :: forall m . ChatMonad m => User -> RcvFileTransfer -> Maybe Bool -> Maybe FilePath -> m AChatItem
2023-03-21 15:21:14 +04:00
acceptFileReceive user @ User { userId } RcvFileTransfer { fileId , xftpRcvFile , fileInvitation = FileInvitation { fileName = fName , fileConnReq , fileInline , fileSize } , fileStatus , grpMemberId } rcvInline_ filePath_ = do
2022-05-11 16:18:28 +04:00
unless ( fileStatus == RFSNew ) $ case fileStatus of
RFSCancelled _ -> throwChatError $ CEFileCancelled fName
2022-12-12 16:33:07 +04:00
_ -> throwChatError $ CEFileAlreadyReceiving fName
2023-03-21 15:21:14 +04:00
case ( xftpRcvFile , fileConnReq ) of
2022-05-11 16:18:28 +04:00
-- direct file protocol
2023-03-13 10:30:32 +00:00
( Nothing , Just connReq ) -> do
2023-09-10 22:40:15 +03:00
subMode <- chatReadVar subscriptionMode
dm <- directMessage $ XFileAcpt fName
connIds <- joinAgentConnectionAsync user True connReq dm subMode
2023-03-16 10:49:57 +04:00
filePath <- getRcvFilePath fileId filePath_ fName True
2023-09-10 22:40:15 +03:00
withStoreCtx ( Just " acceptFileReceive, acceptRcvFileTransfer " ) $ \ db -> acceptRcvFileTransfer db user fileId connIds ConnJoined filePath subMode
2023-03-14 21:51:35 +04:00
-- XFTP
2023-09-01 19:43:27 +01:00
( Just XFTPRcvFile { cryptoArgs } , _ ) -> do
2023-03-16 10:49:57 +04:00
filePath <- getRcvFilePath fileId filePath_ fName False
2023-05-29 15:18:22 +04:00
( ci , rfd ) <- withStoreCtx ( Just " acceptFileReceive, xftpAcceptRcvFT ... " ) $ \ db -> do
2023-04-03 18:49:22 +04:00
-- marking file as accepted and reading description in the same transaction
-- to prevent race condition with appending description
ci <- xftpAcceptRcvFT db user fileId filePath
rfd <- getRcvFileDescrByFileId db fileId
pure ( ci , rfd )
2023-09-01 19:43:27 +01:00
receiveViaCompleteFD user fileId rfd cryptoArgs
2023-03-14 21:51:35 +04:00
pure ci
2022-09-20 14:46:30 +01:00
-- group & direct file protocol
2023-03-13 10:30:32 +00:00
_ -> do
2023-05-29 15:18:22 +04:00
chatRef <- withStoreCtx ( Just " acceptFileReceive, getChatRefByFileId " ) $ \ db -> getChatRefByFileId db user fileId
2022-09-20 14:46:30 +01:00
case ( chatRef , grpMemberId ) of
( ChatRef CTDirect contactId , Nothing ) -> do
2023-05-29 15:18:22 +04:00
ct <- withStoreCtx ( Just " acceptFileReceive, getContact " ) $ \ db -> getContact db user contactId
2022-12-12 16:33:07 +04:00
acceptFile CFCreateConnFileInvDirect $ \ msg -> void $ sendDirectContactMessage ct msg
2022-09-20 14:46:30 +01:00
( ChatRef CTGroup groupId , Just memId ) -> do
2023-05-29 15:18:22 +04:00
GroupMember { activeConn } <- withStoreCtx ( Just " acceptFileReceive, getGroupMember " ) $ \ db -> getGroupMember db user groupId memId
2022-04-10 13:30:58 +04:00
case activeConn of
2022-05-11 16:18:28 +04:00
Just conn -> do
2022-12-12 16:33:07 +04:00
acceptFile CFCreateConnFileInvGroup $ \ msg -> void $ sendDirectMessage conn msg $ GroupId groupId
2022-05-11 16:18:28 +04:00
_ -> throwChatError $ CEFileInternal " member connection not active "
2022-09-20 14:46:30 +01:00
_ -> throwChatError $ CEFileInternal " invalid chat ref for file transfer "
2022-04-10 13:30:58 +04:00
where
2022-12-12 16:33:07 +04:00
acceptFile :: CommandFunction -> ( ChatMsgEvent 'Json -> m () ) -> m AChatItem
acceptFile cmdFunction send = do
2023-03-16 10:49:57 +04:00
filePath <- getRcvFilePath fileId filePath_ fName True
2022-10-20 14:32:20 +01:00
inline <- receiveInline
2022-10-14 13:06:33 +01:00
if
2022-10-20 14:32:20 +01:00
| inline -> do
2022-10-15 14:48:07 +04:00
-- accepting inline
2023-05-29 15:18:22 +04:00
ci <- withStoreCtx ( Just " acceptFile, acceptRcvInlineFT " ) $ \ db -> acceptRcvInlineFT db user fileId filePath
2022-12-12 16:33:07 +04:00
sharedMsgId <- withStore $ \ db -> getSharedMsgIdByFileId db userId fileId
send $ XFileAcptInv sharedMsgId Nothing fName
pure ci
2022-10-14 13:06:33 +01:00
| fileInline == Just IFMSent -> throwChatError $ CEFileAlreadyReceiving fName
| otherwise -> do
2022-10-15 14:48:07 +04:00
-- accepting via a new connection
2023-09-10 22:40:15 +03:00
subMode <- chatReadVar subscriptionMode
connIds <- createAgentConnectionAsync user cmdFunction True SCMInvitation subMode
withStoreCtx ( Just " acceptFile, acceptRcvFileTransfer " ) $ \ db -> acceptRcvFileTransfer db user fileId connIds ConnNew filePath subMode
2022-10-20 14:32:20 +01:00
receiveInline :: m Bool
receiveInline = do
ChatConfig { fileChunkSize , inlineFiles = InlineFilesConfig { receiveChunks , offerChunks } } <- asks config
pure $
rcvInline_ /= Just False
&& fileInline == Just IFMOffer
&& ( fileSize <= fileChunkSize * receiveChunks
|| ( rcvInline_ == Just True && fileSize <= fileChunkSize * offerChunks )
)
2022-10-14 13:06:33 +01:00
2023-09-01 19:43:27 +01:00
receiveViaCompleteFD :: ChatMonad m => User -> FileTransferId -> RcvFileDescr -> Maybe CryptoFileArgs -> m ()
receiveViaCompleteFD user fileId RcvFileDescr { fileDescrText , fileDescrComplete } cfArgs =
2023-03-14 21:51:35 +04:00
when fileDescrComplete $ do
2023-04-14 15:32:12 +04:00
rd <- parseFileDescription fileDescrText
2023-09-01 19:43:27 +01:00
aFileId <- withAgent $ \ a -> xftpReceiveFile a ( aUserId user ) rd cfArgs
2023-03-16 10:49:57 +04:00
startReceivingFile user fileId
2023-05-29 15:18:22 +04:00
withStoreCtx' ( Just " receiveViaCompleteFD, updateRcvFileAgentId " ) $ \ db -> updateRcvFileAgentId db fileId ( Just $ AgentRcvFileId aFileId )
2023-03-14 21:51:35 +04:00
2023-03-16 10:49:57 +04:00
startReceivingFile :: ChatMonad m => User -> FileTransferId -> m ()
startReceivingFile user fileId = do
2023-05-29 15:18:22 +04:00
ci <- withStoreCtx ( Just " startReceivingFile, updateRcvFileStatus ... " ) $ \ db -> do
2023-03-16 10:49:57 +04:00
liftIO $ updateRcvFileStatus db fileId FSConnected
liftIO $ updateCIFileStatus db user fileId $ CIFSRcvTransfer 0 1
getChatItemByFileId db user fileId
toView $ CRRcvFileStart user ci
getRcvFilePath :: forall m . ChatMonad m => FileTransferId -> Maybe FilePath -> String -> Bool -> m FilePath
getRcvFilePath fileId fPath_ fn keepHandle = case fPath_ of
2022-10-14 13:06:33 +01:00
Nothing ->
asks filesFolder >>= readTVarIO >>= \ case
Nothing -> do
dir <- ( ` combine ` " Downloads " ) <$> getHomeDirectory
ifM ( doesDirectoryExist dir ) ( pure dir ) getTemporaryDirectory
>>= ( ` uniqueCombine ` fn )
>>= createEmptyFile
Just filesFolder ->
filesFolder ` uniqueCombine ` fn
>>= createEmptyFile
>>= pure <$> takeFileName
Just fPath ->
ifM
( doesDirectoryExist fPath )
( fPath ` uniqueCombine ` fn >>= createEmptyFile )
$ ifM
( doesFileExist fPath )
( throwChatError $ CEFileAlreadyExists fPath )
( createEmptyFile fPath )
where
createEmptyFile :: FilePath -> m FilePath
2023-07-09 23:24:38 +01:00
createEmptyFile fPath = emptyFile fPath ` catchThrow ` ( ChatError . CEFileWrite fPath . show )
2022-10-14 13:06:33 +01:00
emptyFile :: FilePath -> m FilePath
emptyFile fPath = do
2023-03-16 10:49:57 +04:00
h <-
if keepHandle
then getFileHandle fileId fPath rcvFiles AppendMode
else getTmpHandle fPath
2022-10-14 13:06:33 +01:00
liftIO $ B . hPut h " " >> hFlush h
pure fPath
2023-03-16 10:49:57 +04:00
getTmpHandle :: FilePath -> m Handle
2023-07-09 23:24:38 +01:00
getTmpHandle fPath = openFile fPath AppendMode ` catchThrow ` ( ChatError . CEFileInternal . show )
2022-10-14 13:06:33 +01:00
uniqueCombine :: FilePath -> String -> m FilePath
uniqueCombine filePath fileName = tryCombine ( 0 :: Int )
2021-09-04 07:32:56 +01:00
where
2022-10-14 13:06:33 +01:00
tryCombine n =
let ( name , ext ) = splitExtensions fileName
suffix = if n == 0 then " " else " _ " <> show n
f = filePath ` combine ` ( name <> suffix <> ext )
in ifM ( doesFileExist f ) ( tryCombine $ n + 1 ) ( pure f )
2021-06-25 18:18:24 +01:00
2022-10-14 14:57:01 +04:00
acceptContactRequest :: ChatMonad m => User -> UserContactRequest -> Maybe IncognitoProfile -> m Contact
2023-09-01 19:20:07 +04:00
acceptContactRequest user UserContactRequest { agentInvitationId = AgentInvId invId , cReqChatVRange , localDisplayName = cName , profileId , profile = cp , userContactLinkId , xContactId } incognitoProfile = do
2023-09-10 22:40:15 +03:00
subMode <- chatReadVar subscriptionMode
2022-10-14 14:57:01 +04:00
let profileToSend = profileToSendOnAccept user incognitoProfile
2023-09-01 19:20:07 +04:00
dm <- directMessage $ XInfo profileToSend
2023-09-10 22:40:15 +03:00
acId <- withAgent $ \ a -> acceptContact a True invId dm subMode
2023-09-12 17:59:09 +04:00
withStore' $ \ db -> createAcceptedContact db user acId ( fromJVersionRange cReqChatVRange ) cName profileId cp userContactLinkId xContactId incognitoProfile subMode
2022-10-14 14:57:01 +04:00
acceptContactRequestAsync :: ChatMonad m => User -> UserContactRequest -> Maybe IncognitoProfile -> m Contact
2023-09-01 19:20:07 +04:00
acceptContactRequestAsync user UserContactRequest { agentInvitationId = AgentInvId invId , cReqChatVRange , localDisplayName = cName , profileId , profile = p , userContactLinkId , xContactId } incognitoProfile = do
2023-09-10 22:40:15 +03:00
subMode <- chatReadVar subscriptionMode
2022-10-14 14:57:01 +04:00
let profileToSend = profileToSendOnAccept user incognitoProfile
2023-09-10 22:40:15 +03:00
( cmdId , acId ) <- agentAcceptContactAsync user True invId ( XInfo profileToSend ) subMode
2022-10-14 14:57:01 +04:00
withStore' $ \ db -> do
2023-09-12 17:59:09 +04:00
ct @ Contact { activeConn = Connection { connId } } <- createAcceptedContact db user acId ( fromJVersionRange cReqChatVRange ) cName profileId p userContactLinkId xContactId incognitoProfile subMode
2022-10-14 14:57:01 +04:00
setCommandConnId db user cmdId connId
pure ct
profileToSendOnAccept :: User -> Maybe IncognitoProfile -> Profile
2022-11-01 17:32:49 +03:00
profileToSendOnAccept user ip = userProfileToSend user ( getIncognitoProfile <$> ip ) Nothing
where
getIncognitoProfile = \ case
NewIncognito p -> p
ExistingIncognito lp -> fromLocalProfile lp
2022-02-14 14:59:11 +04:00
2022-10-13 17:12:22 +04:00
deleteGroupLink' :: ChatMonad m => User -> GroupInfo -> m ()
deleteGroupLink' user gInfo = do
conn <- withStore $ \ db -> getGroupLinkConnection db user gInfo
2023-02-01 13:57:39 +00:00
deleteGroupLink_ user gInfo conn
deleteGroupLinkIfExists :: ChatMonad m => User -> GroupInfo -> m ()
deleteGroupLinkIfExists user gInfo = do
conn_ <- eitherToMaybe <$> withStore' ( \ db -> runExceptT $ getGroupLinkConnection db user gInfo )
mapM_ ( deleteGroupLink_ user gInfo ) conn_
deleteGroupLink_ :: ChatMonad m => User -> GroupInfo -> Connection -> m ()
deleteGroupLink_ user gInfo conn = do
2023-01-24 16:24:34 +04:00
deleteAgentConnectionAsync user $ aConnId conn
2022-10-13 17:12:22 +04:00
withStore' $ \ db -> deleteGroupLink db user gInfo
2023-03-13 10:30:32 +00:00
agentSubscriber :: forall m . ( MonadUnliftIO m , MonadReader ChatController m ) => m ()
2022-07-02 10:13:06 +01:00
agentSubscriber = do
2021-07-06 19:07:03 +01:00
q <- asks $ subQ . smpAgent
2021-08-05 20:51:48 +01:00
l <- asks chatLock
2023-03-13 10:30:32 +00:00
forever $ atomically ( readTBQueue q ) >>= void . process l
2022-10-22 21:22:44 +01:00
where
2023-03-13 10:30:32 +00:00
process :: Lock -> ( ACorrId , EntityId , APartyCmd 'Agent ) -> m ( Either ChatError () )
process l ( corrId , entId , APC 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 = do
let name = " agentSubscriber entity= " <> show e <> " entId= " <> str entId <> " msg= " <> str ( aCommandTag msg )
2023-07-09 23:24:38 +01:00
withLock l name $ runExceptT $ action ` catchChatError ` ( toView . CRChatError Nothing )
2023-03-13 10:30:32 +00:00
str :: StrEncoding a => a -> String
str = B . unpack . strEncode
2022-02-06 16:18:01 +00:00
2022-07-17 15:51:17 +01:00
type AgentBatchSubscribe m = AgentClient -> [ ConnId ] -> ExceptT AgentErrorType m ( Map ConnId ( Either AgentErrorType () ) )
2023-09-10 22:40:15 +03:00
subscribeUserConnections :: forall m . ChatMonad m => Bool -> AgentBatchSubscribe m -> User -> m ()
subscribeUserConnections onlyNeeded agentBatchSubscribe user @ User { userId } = do
2022-07-17 15:51:17 +01:00
-- get user connections
2022-02-25 16:29:36 +04:00
ce <- asks $ subscriptionEvents . config
2023-09-10 22:40:15 +03:00
( conns , cts , ucs , gs , ms , sfts , rfts , pcs ) <-
if onlyNeeded
then do
( conns , entities ) <- withStore' getConnectionsToSubscribe
let ( cts , ucs , ms , sfts , rfts , pcs ) = foldl' addEntity ( M . empty , M . empty , M . empty , M . empty , M . empty , M . empty ) entities
pure ( conns , cts , ucs , [] , ms , sfts , rfts , pcs )
else do
withStore' unsetConnectionToSubscribe
( ctConns , cts ) <- getContactConns
( ucConns , ucs ) <- getUserContactLinkConns
( gs , mConns , ms ) <- getGroupMemberConns
( sftConns , sfts ) <- getSndFileTransferConns
( rftConns , rfts ) <- getRcvFileTransferConns
( pcConns , pcs ) <- getPendingContactConns
let conns = concat [ ctConns , ucConns , mConns , sftConns , rftConns , pcConns ]
pure ( conns , cts , ucs , gs , ms , sfts , rfts , pcs )
2022-07-17 15:51:17 +01:00
-- subscribe using batched commands
2023-09-10 22:40:15 +03:00
rs <- withAgent $ \ a -> agentBatchSubscribe a conns
2022-07-17 15:51:17 +01:00
-- send connection events to view
2023-02-04 23:13:20 +00:00
contactSubsToView rs cts ce
2022-07-17 15:51:17 +01:00
contactLinkSubsToView rs ucs
groupSubsToView rs gs ms ce
sndFileSubsToView rs sfts
rcvFileSubsToView rs rfts
pendingConnSubsToView rs pcs
2021-07-25 20:23:52 +01:00
where
2023-09-10 22:40:15 +03:00
addEntity ( cts , ucs , ms , sfts , rfts , pcs ) = \ case
RcvDirectMsgConnection c ( Just ct ) -> let cts' = addConn c ct cts in ( cts' , ucs , ms , sfts , rfts , pcs )
RcvDirectMsgConnection c Nothing -> let pcs' = addConn c ( toPCC c ) pcs in ( cts , ucs , ms , sfts , rfts , pcs' )
RcvGroupMsgConnection c _g m -> let ms' = addConn c m ms in ( cts , ucs , ms' , sfts , rfts , pcs )
SndFileConnection c sft -> let sfts' = addConn c sft sfts in ( cts , ucs , ms , sfts' , rfts , pcs )
RcvFileConnection c rft -> let rfts' = addConn c rft rfts in ( cts , ucs , ms , sfts , rfts' , pcs )
UserContactConnection c uc -> let ucs' = addConn c uc ucs in ( cts , ucs' , ms , sfts , rfts , pcs )
addConn :: Connection -> a -> Map ConnId a -> Map ConnId a
addConn = M . insert . aConnId
toPCC Connection { connId , agentConnId , connStatus , viaUserContactLink , groupLinkId , customUserProfileId , localAlias , createdAt } =
PendingContactConnection
{ pccConnId = connId ,
pccAgentConnId = agentConnId ,
pccConnStatus = connStatus ,
viaContactUri = False ,
viaUserContactLink ,
groupLinkId ,
customUserProfileId ,
connReqInv = Nothing ,
localAlias ,
createdAt ,
updatedAt = createdAt
}
2022-07-17 15:51:17 +01:00
getContactConns :: m ( [ ConnId ] , Map ConnId Contact )
getContactConns = do
2023-05-29 15:18:22 +04:00
cts <- withStore_ ( " subscribeUserConnections " <> show userId <> " , getUserContacts " ) getUserContacts
2022-07-17 15:51:17 +01:00
let connIds = map contactConnId cts
pure ( connIds , M . fromList $ zip connIds cts )
getUserContactLinkConns :: m ( [ ConnId ] , Map ConnId UserContact )
getUserContactLinkConns = do
2023-05-29 15:18:22 +04:00
( cs , ucs ) <- unzip <$> withStore_ ( " subscribeUserConnections " <> show userId <> " , getUserContactLinks " ) getUserContactLinks
2022-07-17 15:51:17 +01:00
let connIds = map aConnId cs
pure ( connIds , M . fromList $ zip connIds ucs )
getGroupMemberConns :: m ( [ Group ] , [ ConnId ] , Map ConnId GroupMember )
getGroupMemberConns = do
2023-05-29 15:18:22 +04:00
gs <- withStore_ ( " subscribeUserConnections " <> show userId <> " , getUserGroups " ) getUserGroups
2022-07-17 15:51:17 +01:00
let mPairs = concatMap ( \ ( Group _ ms ) -> mapMaybe ( \ m -> ( , m ) <$> memberConnId m ) ms ) gs
pure ( gs , map fst mPairs , M . fromList mPairs )
getSndFileTransferConns :: m ( [ ConnId ] , Map ConnId SndFileTransfer )
getSndFileTransferConns = do
2023-05-29 15:18:22 +04:00
sfts <- withStore_ ( " subscribeUserConnections " <> show userId <> " , getLiveSndFileTransfers " ) getLiveSndFileTransfers
2022-07-17 15:51:17 +01:00
let connIds = map sndFileTransferConnId sfts
pure ( connIds , M . fromList $ zip connIds sfts )
getRcvFileTransferConns :: m ( [ ConnId ] , Map ConnId RcvFileTransfer )
getRcvFileTransferConns = do
2023-05-29 15:18:22 +04:00
rfts <- withStore_ ( " subscribeUserConnections " <> show userId <> " , getLiveRcvFileTransfers " ) getLiveRcvFileTransfers
2022-07-17 15:51:17 +01:00
let rftPairs = mapMaybe ( \ ft -> ( , ft ) <$> liveRcvFileTransferConnId ft ) rfts
pure ( map fst rftPairs , M . fromList rftPairs )
getPendingContactConns :: m ( [ ConnId ] , Map ConnId PendingContactConnection )
getPendingContactConns = do
2023-05-29 15:18:22 +04:00
pcs <- withStore_ ( " subscribeUserConnections " <> show userId <> " , getPendingContactConnections " ) getPendingContactConnections
2022-07-17 15:51:17 +01:00
let connIds = map aConnId' pcs
pure ( connIds , M . fromList $ zip connIds pcs )
2023-02-04 23:13:20 +00:00
contactSubsToView :: Map ConnId ( Either AgentErrorType () ) -> Map ConnId Contact -> Bool -> m ()
contactSubsToView rs cts ce = do
toView . CRContactSubSummary user $ map ( uncurry ContactSubStatus ) cRs
when ce $ mapM_ ( toView . uncurry ( CRContactSubError user ) ) cErrors
where
cRs = resultsFor rs cts
cErrors = sortOn ( \ ( Contact { localDisplayName = n } , _ ) -> n ) $ filterErrors cRs
2022-07-17 15:51:17 +01:00
contactLinkSubsToView :: Map ConnId ( Either AgentErrorType () ) -> Map ConnId UserContact -> m ()
2023-01-14 15:45:13 +04:00
contactLinkSubsToView rs = toView . CRUserContactSubSummary user . map ( uncurry UserContactSubStatus ) . resultsFor rs
2022-07-17 15:51:17 +01:00
groupSubsToView :: Map ConnId ( Either AgentErrorType () ) -> [ Group ] -> Map ConnId GroupMember -> Bool -> m ()
groupSubsToView rs gs ms ce = do
mapM_ groupSub $
2022-08-18 11:35:31 +04:00
sortOn ( \ ( Group GroupInfo { localDisplayName = g } _ ) -> g ) gs
2023-01-14 15:45:13 +04:00
toView . CRMemberSubSummary user $ map ( uncurry MemberSubStatus ) mRs
2022-07-17 15:51:17 +01:00
where
mRs = resultsFor rs ms
groupSub :: Group -> m ()
groupSub ( Group g @ GroupInfo { membership , groupId = gId } members ) = do
2023-01-14 15:45:13 +04:00
when ce $ mapM_ ( toView . uncurry ( CRMemberSubError user g ) ) mErrors
2022-07-17 15:51:17 +01:00
toView groupEvent
where
mErrors :: [ ( GroupMember , ChatError ) ]
mErrors =
2022-07-20 16:56:55 +04:00
sortOn ( \ ( GroupMember { localDisplayName = n } , _ ) -> n )
2022-07-17 15:51:17 +01:00
. filterErrors
$ filter ( \ ( GroupMember { groupId } , _ ) -> groupId == gId ) mRs
groupEvent :: ChatResponse
groupEvent
2023-01-14 15:45:13 +04:00
| memberStatus membership == GSMemInvited = CRGroupInvitation user g
2022-07-17 15:51:17 +01:00
| all ( \ GroupMember { activeConn } -> isNothing activeConn ) members =
2022-10-15 14:48:07 +04:00
if memberActive membership
2023-01-14 15:45:13 +04:00
then CRGroupEmpty user g
else CRGroupRemoved user g
| otherwise = CRGroupSubscribed user g
2022-07-17 15:51:17 +01:00
sndFileSubsToView :: Map ConnId ( Either AgentErrorType () ) -> Map ConnId SndFileTransfer -> m ()
sndFileSubsToView rs sfts = do
let sftRs = resultsFor rs sfts
forM_ sftRs $ \ ( ft @ SndFileTransfer { fileId , fileStatus } , err_ ) -> do
2023-01-14 15:45:13 +04:00
forM_ err_ $ toView . CRSndFileSubError user ft
2022-07-17 15:51:17 +01:00
void . forkIO $ do
threadDelay 1000000
l <- asks chatLock
2022-10-22 21:22:44 +01:00
when ( fileStatus == FSConnected ) . unlessM ( isFileActive fileId sndFiles ) . withLock l " subscribe sendFileChunk " $
2022-10-04 17:19:00 +01:00
sendFileChunk user ft
2022-07-17 15:51:17 +01:00
rcvFileSubsToView :: Map ConnId ( Either AgentErrorType () ) -> Map ConnId RcvFileTransfer -> m ()
2023-01-14 15:45:13 +04:00
rcvFileSubsToView rs = mapM_ ( toView . uncurry ( CRRcvFileSubError user ) ) . filterErrors . resultsFor rs
2022-07-17 15:51:17 +01:00
pendingConnSubsToView :: Map ConnId ( Either AgentErrorType () ) -> Map ConnId PendingContactConnection -> m ()
2023-01-14 15:45:13 +04:00
pendingConnSubsToView rs = toView . CRPendingSubSummary user . map ( uncurry PendingSubStatus ) . resultsFor rs
2023-05-29 15:18:22 +04:00
withStore_ :: String -> ( DB . Connection -> User -> IO [ a ] ) -> m [ a ]
2023-07-09 23:24:38 +01:00
withStore_ ctx a = withStoreCtx' ( Just ctx ) ( ` a ` user ) ` catchChatError ` \ e -> toView ( CRChatError ( Just user ) e ) $> []
2022-07-17 15:51:17 +01:00
filterErrors :: [ ( a , Maybe ChatError ) ] -> [ ( a , ChatError ) ]
filterErrors = mapMaybe ( \ ( a , e_ ) -> ( a , ) <$> e_ )
resultsFor :: Map ConnId ( Either AgentErrorType () ) -> Map ConnId a -> [ ( a , Maybe ChatError ) ]
resultsFor rs = M . foldrWithKey' addResult []
2021-09-04 07:32:56 +01:00
where
2022-07-17 15:51:17 +01:00
addResult :: ConnId -> a -> [ ( a , Maybe ChatError ) ] -> [ ( a , Maybe ChatError ) ]
addResult connId = ( : ) . ( , err )
2021-09-04 07:32:56 +01:00
where
2022-07-17 15:51:17 +01:00
err = case M . lookup connId rs of
2023-01-06 13:11:21 +04:00
Just ( Left e ) -> Just $ ChatErrorAgent e Nothing
2022-07-17 15:51:17 +01:00
Just _ -> Nothing
_ -> Just . ChatError . CEAgentNoSubResult $ AgentConnId connId
2021-07-25 20:23:52 +01:00
2023-01-14 19:21:10 +04:00
cleanupManager :: forall m . ChatMonad m => m ()
cleanupManager = do
2023-05-26 14:03:26 +04:00
interval <- asks ( cleanupManagerInterval . config )
runWithoutInitialDelay interval
2023-06-12 13:45:39 +04:00
initialDelay <- asks ( initialCleanupManagerDelay . config )
liftIO $ threadDelay' initialDelay
stepDelay <- asks ( cleanupManagerStepDelay . config )
2022-12-15 15:17:29 +04:00
forever $ do
2023-07-09 23:24:38 +01:00
flip catchChatError ( toView . CRChatError Nothing ) $ do
2022-12-22 19:18:38 +04:00
waitChatStarted
2023-05-29 15:18:22 +04:00
users <- withStoreCtx' ( Just " cleanupManager, getUsers 1 " ) getUsers
2023-01-14 19:21:10 +04:00
let ( us , us' ) = partition activeUser users
2023-06-12 13:45:39 +04:00
forM_ us $ cleanupUser interval stepDelay
forM_ us' $ cleanupUser interval stepDelay
2023-07-09 23:24:38 +01:00
cleanupMessages ` catchChatError ` ( toView . CRChatError Nothing )
2023-05-26 14:03:26 +04:00
liftIO $ threadDelay' $ diffToMicroseconds interval
2022-12-15 15:17:29 +04:00
where
2023-07-09 23:24:38 +01:00
runWithoutInitialDelay cleanupInterval = flip catchChatError ( toView . CRChatError Nothing ) $ do
2023-05-26 14:03:26 +04:00
waitChatStarted
2023-05-29 15:18:22 +04:00
users <- withStoreCtx' ( Just " cleanupManager, getUsers 2 " ) getUsers
2023-05-26 14:03:26 +04:00
let ( us , us' ) = partition activeUser users
2023-07-09 23:24:38 +01:00
forM_ us $ \ u -> cleanupTimedItems cleanupInterval u ` catchChatError ` ( toView . CRChatError ( Just u ) )
forM_ us' $ \ u -> cleanupTimedItems cleanupInterval u ` catchChatError ` ( toView . CRChatError ( Just u ) )
2023-06-12 13:45:39 +04:00
cleanupUser cleanupInterval stepDelay user = do
2023-07-09 23:24:38 +01:00
cleanupTimedItems cleanupInterval user ` catchChatError ` ( toView . CRChatError ( Just user ) )
2023-06-12 13:45:39 +04:00
liftIO $ threadDelay' stepDelay
2023-07-09 23:24:38 +01:00
cleanupDeletedContacts user ` catchChatError ` ( toView . CRChatError ( Just user ) )
2023-06-12 13:45:39 +04:00
liftIO $ threadDelay' stepDelay
2023-05-26 14:03:26 +04:00
cleanupTimedItems cleanupInterval user = do
2022-12-15 15:17:29 +04:00
ts <- liftIO getCurrentTime
2023-05-26 14:03:26 +04:00
let startTimedThreadCutoff = addUTCTime cleanupInterval ts
2023-05-29 15:18:22 +04:00
timedItems <- withStoreCtx' ( Just " cleanupManager, getTimedItems " ) $ \ db -> getTimedItems db user startTimedThreadCutoff
2023-07-09 23:24:38 +01:00
forM_ timedItems $ \ ( itemRef , deleteAt ) -> startTimedItemThread user itemRef deleteAt ` catchChatError ` const ( pure () )
2023-06-12 13:45:39 +04:00
cleanupDeletedContacts user = do
contacts <- withStore' ( ` getDeletedContacts ` user )
forM_ contacts $ \ ct ->
withStore' ( \ db -> deleteContactWithoutGroups db user ct )
2023-07-09 23:24:38 +01:00
` catchChatError ` ( toView . CRChatError ( Just user ) )
2023-05-05 13:49:09 +04:00
cleanupMessages = do
ts <- liftIO getCurrentTime
let cutoffTs = addUTCTime ( - ( 30 * nominalDay ) ) ts
2023-05-29 15:18:22 +04:00
withStoreCtx' ( Just " cleanupManager, deleteOldMessages " ) ( ` deleteOldMessages ` cutoffTs )
2022-12-15 15:17:29 +04:00
2022-12-20 10:17:29 +00:00
startProximateTimedItemThread :: ChatMonad m => User -> ( ChatRef , ChatItemId ) -> UTCTime -> m ()
startProximateTimedItemThread user itemRef deleteAt = do
2023-05-26 14:03:26 +04:00
interval <- asks ( cleanupManagerInterval . config )
2022-12-20 10:17:29 +00:00
ts <- liftIO getCurrentTime
2023-05-26 14:03:26 +04:00
when ( diffUTCTime deleteAt ts <= interval ) $
2022-12-20 10:17:29 +00:00
startTimedItemThread user itemRef deleteAt
2022-12-15 15:17:29 +04:00
startTimedItemThread :: ChatMonad m => User -> ( ChatRef , ChatItemId ) -> UTCTime -> m ()
startTimedItemThread user itemRef deleteAt = do
itemThreads <- asks timedItemThreads
threadTVar_ <- atomically $ do
exists <- TM . member itemRef itemThreads
2022-12-17 14:49:03 +04:00
if not exists
2022-12-15 15:17:29 +04:00
then do
threadTVar <- newTVar Nothing
TM . insert itemRef threadTVar itemThreads
pure $ Just threadTVar
else pure Nothing
forM_ threadTVar_ $ \ threadTVar -> do
tId <- mkWeakThreadId =<< deleteTimedItem user itemRef deleteAt ` forkFinally ` const ( atomically $ TM . delete itemRef itemThreads )
atomically $ writeTVar threadTVar ( Just tId )
deleteTimedItem :: ChatMonad m => User -> ( ChatRef , ChatItemId ) -> UTCTime -> m ()
2022-12-20 12:58:15 +00:00
deleteTimedItem user ( ChatRef cType chatId , itemId ) deleteAt = do
2022-12-15 15:17:29 +04:00
ts <- liftIO getCurrentTime
2023-05-15 21:07:03 +04:00
liftIO $ threadDelay' $ diffToMicroseconds $ diffUTCTime deleteAt ts
2022-12-22 19:18:38 +04:00
waitChatStarted
2022-12-15 15:17:29 +04:00
case cType of
CTDirect -> do
2023-05-29 15:18:22 +04:00
( ct , ci ) <- withStoreCtx ( Just " deleteTimedItem, getContact ... " ) $ \ db -> ( , ) <$> getContact db user chatId <*> getDirectChatItem db user chatId itemId
2022-12-15 15:17:29 +04:00
deleteDirectCI user ct ci True True >>= toView
CTGroup -> do
2023-05-29 15:18:22 +04:00
( gInfo , ci ) <- withStoreCtx ( Just " deleteTimedItem, getGroupInfo ... " ) $ \ db -> ( , ) <$> getGroupInfo db user chatId <*> getGroupChatItem db user chatId itemId
2023-05-19 14:52:51 +02:00
deletedTs <- liftIO getCurrentTime
deleteGroupCI user gInfo ci True True Nothing deletedTs >>= toView
2023-01-04 21:06:28 +04:00
_ -> toView . CRChatError ( Just user ) . ChatError $ CEInternalError " bad deleteTimedItem cType "
2022-12-15 15:17:29 +04:00
2022-12-20 10:17:29 +00:00
startUpdatedTimedItemThread :: ChatMonad m => User -> ChatRef -> ChatItem c d -> ChatItem c d -> m ()
startUpdatedTimedItemThread user chatRef ci ci' =
2023-05-15 21:07:03 +04:00
case ( chatItemTimed ci >>= timedDeleteAt' , chatItemTimed ci' >>= timedDeleteAt' ) of
2022-12-20 10:17:29 +00:00
( Nothing , Just deleteAt' ) ->
startProximateTimedItemThread user ( chatRef , chatItemId' ci' ) deleteAt'
_ -> pure ()
2022-09-28 20:47:06 +04:00
expireChatItems :: forall m . ChatMonad m => User -> Int64 -> Bool -> m ()
2023-01-13 21:01:26 +04:00
expireChatItems user @ User { userId } ttl sync = do
2022-09-28 20:47:06 +04:00
currentTs <- liftIO getCurrentTime
let expirationDate = addUTCTime ( - 1 * fromIntegral ttl ) currentTs
2022-10-05 19:54:28 +04:00
-- this is to keep group messages created during last 12 hours even if they're expired according to item_ts
createdAtCutoff = addUTCTime ( - 43200 :: NominalDiffTime ) currentTs
2023-05-29 15:18:22 +04:00
contacts <- withStoreCtx' ( Just " expireChatItems, getUserContacts " ) ( ` getUserContacts ` user )
2023-01-13 21:01:26 +04:00
loop contacts $ processContact expirationDate
2023-08-01 20:54:51 +01:00
groups <- withStoreCtx' ( Just " expireChatItems, getUserGroupDetails " ) ( \ db -> getUserGroupDetails db user Nothing Nothing )
2023-01-13 21:01:26 +04:00
loop groups $ processGroup expirationDate createdAtCutoff
2022-09-28 20:47:06 +04:00
where
2023-01-13 21:01:26 +04:00
loop :: [ a ] -> ( a -> m () ) -> m ()
loop [] _ = pure ()
loop ( a : as ) process = continue $ do
2023-07-09 23:24:38 +01:00
process a ` catchChatError ` ( toView . CRChatError ( Just user ) )
2023-01-13 21:01:26 +04:00
loop as process
continue :: m () -> m ()
continue a =
if sync
then a
else do
expireFlags <- asks expireCIFlags
expire <- atomically $ TM . lookup userId expireFlags
when ( expire == Just True ) $ threadDelay 100000 >> a
2022-10-06 14:00:02 +04:00
processContact :: UTCTime -> Contact -> m ()
processContact expirationDate ct = do
2023-05-29 15:18:22 +04:00
filesInfo <- withStoreCtx' ( Just " processContact, getContactExpiredFileInfo " ) $ \ db -> getContactExpiredFileInfo db user ct expirationDate
2023-01-24 16:24:34 +04:00
deleteFilesAndConns user filesInfo
2023-05-29 15:18:22 +04:00
withStoreCtx' ( Just " processContact, deleteContactExpiredCIs " ) $ \ db -> deleteContactExpiredCIs db user ct expirationDate
2022-10-06 14:00:02 +04:00
processGroup :: UTCTime -> UTCTime -> GroupInfo -> m ()
processGroup expirationDate createdAtCutoff gInfo = do
2023-05-29 15:18:22 +04:00
filesInfo <- withStoreCtx' ( Just " processGroup, getGroupExpiredFileInfo " ) $ \ db -> getGroupExpiredFileInfo db user gInfo expirationDate createdAtCutoff
2023-01-24 16:24:34 +04:00
deleteFilesAndConns user filesInfo
2023-05-29 15:18:22 +04:00
withStoreCtx' ( Just " processGroup, deleteGroupExpiredCIs " ) $ \ db -> deleteGroupExpiredCIs db user gInfo expirationDate createdAtCutoff
membersToDelete <- withStoreCtx' ( Just " processGroup, getGroupMembersForExpiration " ) $ \ db -> getGroupMembersForExpiration db user gInfo
forM_ membersToDelete $ \ m -> withStoreCtx' ( Just " processGroup, deleteGroupMember " ) $ \ db -> deleteGroupMember db user m
2022-09-28 20:47:06 +04:00
2023-03-13 10:30:32 +00:00
processAgentMessage :: forall m . ChatMonad m => ACorrId -> ConnId -> ACommand 'Agent 'AEConn -> m ()
2023-01-24 20:07:35 +04:00
processAgentMessage _ connId ( DEL_RCVQ srv qId err_ ) =
toView $ CRAgentRcvQueueDeleted ( AgentConnId connId ) srv ( AgentQueueId qId ) err_
processAgentMessage _ connId DEL_CONN =
toView $ CRAgentConnDeleted ( AgentConnId connId )
2023-01-04 21:06:28 +04:00
processAgentMessage corrId connId msg =
withStore' ( ` getUserByAConnId ` AgentConnId connId ) >>= \ case
2023-07-09 23:24:38 +01:00
Just user -> processAgentMessageConn user corrId connId msg ` catchChatError ` ( toView . CRChatError ( Just user ) )
2023-01-04 21:06:28 +04:00
_ -> throwChatError $ CENoConnectionUser ( AgentConnId connId )
2023-03-13 10:30:32 +00:00
processAgentMessageNoConn :: forall m . ChatMonad m => ACommand 'Agent 'AENone -> m ()
2023-01-20 15:02:27 +04:00
processAgentMessageNoConn = \ case
2022-08-13 14:18:12 +01:00
CONNECT p h -> hostEvent $ CRHostConnected p h
DISCONNECT p h -> hostEvent $ CRHostDisconnected p h
2022-04-25 09:17:12 +01:00
DOWN srv conns -> serverEvent srv conns CRContactsDisconnected " disconnected "
UP srv conns -> serverEvent srv conns CRContactsSubscribed " connected "
2022-06-26 15:04:44 +01:00
SUSPENDED -> toView CRChatSuspended
2023-01-24 20:07:35 +04:00
DEL_USER agentUserId -> toView $ CRAgentUserDeleted agentUserId
2022-04-25 09:17:12 +01:00
where
2023-03-10 17:23:04 +00:00
hostEvent :: ChatResponse -> m ()
2022-08-13 14:18:12 +01:00
hostEvent = whenM ( asks $ hostEvents . config ) . toView
2022-08-13 11:53:53 +01:00
serverEvent srv @ ( SMPServer host _ _ ) conns event str = do
2023-01-20 15:02:27 +04:00
cs <- withStore' $ \ db -> getConnectionsContacts db conns
toView $ event srv cs
2022-08-13 11:53:53 +01:00
showToast ( " server " <> str ) ( safeDecodeUtf8 $ strEncode host )
2023-01-04 21:06:28 +04:00
2023-03-13 10:30:32 +00:00
processAgentMsgSndFile :: forall m . ChatMonad m => ACorrId -> SndFileId -> ACommand 'Agent 'AESndFile -> m ()
processAgentMsgSndFile _corrId aFileId msg =
withStore' ( ` getUserByASndFileId ` AgentSndFileId aFileId ) >>= \ case
2023-07-09 23:24:38 +01:00
Just user -> process user ` catchChatError ` ( toView . CRChatError ( Just user ) )
2023-04-25 15:46:00 +04:00
_ -> do
withAgent ( ` xftpDeleteSndFileInternal ` aFileId )
throwChatError $ CENoSndFileUser $ AgentSndFileId aFileId
2023-03-13 10:30:32 +00:00
where
process :: User -> m ()
process user = do
2023-03-30 14:10:13 +04:00
( ft @ FileTransferMeta { fileId , cancelled } , sfts ) <- withStore $ \ db -> do
fileId <- getXFTPSndFileDBId db user $ AgentSndFileId aFileId
getSndFileTransfer db user fileId
2023-04-18 12:48:36 +04:00
unless cancelled $ case msg of
SFPROG sndProgress sndTotal -> do
let status = CIFSSndTransfer { sndProgress , sndTotal }
ci <- withStore $ \ db -> do
liftIO $ updateCIFileStatus db user fileId status
getChatItemByFileId db user fileId
toView $ CRSndFileProgressXFTP user ci ft sndProgress sndTotal
SFDONE sndDescr rfds -> do
withStore' $ \ db -> setSndFTPrivateSndDescr db user fileId ( fileDescrText sndDescr )
ci @ ( AChatItem _ d cInfo _ci @ ChatItem { meta = CIMeta { itemSharedMsgId = msgId_ , itemDeleted } } ) <-
withStore $ \ db -> getChatItemByFileId db user fileId
case ( msgId_ , itemDeleted ) of
( Just sharedMsgId , Nothing ) -> do
when ( length rfds < length sfts ) $ throwChatError $ CEInternalError " not enough XFTP file descriptions to send "
-- TODO either update database status or move to SFPROG
toView $ CRSndFileProgressXFTP user ci ft 1 1
case ( rfds , sfts , d , cInfo ) of
( rfd : extraRFDs , sft : _ , SMDSnd , DirectChat ct ) -> do
withStore' $ \ db -> createExtraSndFTDescrs db user fileId ( map fileDescrText extraRFDs )
msgDeliveryId <- sendFileDescription sft rfd sharedMsgId $ sendDirectContactMessage ct
withStore' $ \ db -> updateSndFTDeliveryXFTP db sft msgDeliveryId
2023-04-25 15:46:00 +04:00
withAgent ( ` xftpDeleteSndFileInternal ` aFileId )
2023-04-18 12:48:36 +04:00
( _ , _ , SMDSnd , GroupChat g @ GroupInfo { groupId } ) -> do
ms <- withStore' $ \ db -> getGroupMembers db user g
let rfdsMemberFTs = zip rfds $ memberFTs ms
extraRFDs = drop ( length rfdsMemberFTs ) rfds
withStore' $ \ db -> createExtraSndFTDescrs db user fileId ( map fileDescrText extraRFDs )
2023-07-09 23:24:38 +01:00
forM_ rfdsMemberFTs $ \ mt -> sendToMember mt ` catchChatError ` ( toView . CRChatError ( Just user ) )
2023-04-18 12:48:36 +04:00
ci' <- withStore $ \ db -> do
liftIO $ updateCIFileStatus db user fileId CIFSSndComplete
getChatItemByFileId db user fileId
2023-04-25 15:46:00 +04:00
withAgent ( ` xftpDeleteSndFileInternal ` aFileId )
2023-04-18 12:48:36 +04:00
toView $ CRSndFileCompleteXFTP user ci' ft
where
memberFTs :: [ GroupMember ] -> [ ( Connection , SndFileTransfer ) ]
memberFTs ms = M . elems $ M . intersectionWith ( , ) ( M . fromList mConns' ) ( M . fromList sfts' )
where
mConns' = mapMaybe useMember ms
sfts' = mapMaybe ( \ sft @ SndFileTransfer { groupMemberId } -> ( , sft ) <$> groupMemberId ) sfts
useMember GroupMember { groupMemberId , activeConn = Just conn @ Connection { connStatus } }
| ( connStatus == ConnReady || connStatus == ConnSndReady ) && not ( connDisabled conn ) = Just ( groupMemberId , conn )
| otherwise = Nothing
useMember _ = Nothing
sendToMember :: ( ValidFileDescription 'FRecipient , ( Connection , SndFileTransfer ) ) -> m ()
sendToMember ( rfd , ( conn , sft ) ) =
void $ sendFileDescription sft rfd sharedMsgId $ \ msg' -> sendDirectMessage conn msg' $ GroupId groupId
_ -> pure ()
_ -> pure () -- TODO error?
SFERR e
| temporaryAgentError e ->
throwChatError $ CEXFTPSndFile fileId ( AgentSndFileId aFileId ) e
| otherwise -> do
2023-03-30 14:10:13 +04:00
ci <- withStore $ \ db -> do
2023-04-18 12:48:36 +04:00
liftIO $ updateFileCancelled db user fileId CIFSSndError
2023-03-30 14:10:13 +04:00
getChatItemByFileId db user fileId
2023-04-25 15:46:00 +04:00
withAgent ( ` xftpDeleteSndFileInternal ` aFileId )
2023-04-18 12:48:36 +04:00
toView $ CRSndFileError user ci
2023-03-13 10:30:32 +00:00
where
2023-04-14 15:32:12 +04:00
fileDescrText :: FilePartyI p => ValidFileDescription p -> T . Text
2023-04-12 14:47:54 +04:00
fileDescrText = safeDecodeUtf8 . strEncode
2023-03-16 13:58:01 +00:00
sendFileDescription :: SndFileTransfer -> ValidFileDescription 'FRecipient -> SharedMsgId -> ( ChatMsgEvent 'Json -> m ( SndMessage , Int64 ) ) -> m Int64
2023-03-14 09:28:54 +00:00
sendFileDescription sft rfd msgId sendMsg = do
2023-04-12 14:47:54 +04:00
let rfdText = fileDescrText rfd
2023-03-14 09:28:54 +00:00
withStore' $ \ db -> updateSndFTDescrXFTP db user sft rfdText
2023-03-13 10:30:32 +00:00
partSize <- asks $ xftpDescrPartSize . config
2023-03-16 13:58:01 +00:00
sendParts 1 partSize rfdText
2023-03-13 10:30:32 +00:00
where
2023-03-14 09:28:54 +00:00
sendParts partNo partSize rfdText = do
let ( part , rest ) = T . splitAt partSize rfdText
2023-03-13 10:30:32 +00:00
complete = T . null rest
fileDescr = FileDescr { fileDescrText = part , fileDescrPartNo = partNo , fileDescrComplete = complete }
( _ , msgDeliveryId ) <- sendMsg $ XMsgFileDescr { msgId , fileDescr }
if complete
then pure msgDeliveryId
else sendParts ( partNo + 1 ) partSize rest
processAgentMsgRcvFile :: forall m . ChatMonad m => ACorrId -> RcvFileId -> ACommand 'Agent 'AERcvFile -> m ()
processAgentMsgRcvFile _corrId aFileId msg =
withStore' ( ` getUserByARcvFileId ` AgentRcvFileId aFileId ) >>= \ case
2023-07-09 23:24:38 +01:00
Just user -> process user ` catchChatError ` ( toView . CRChatError ( Just user ) )
2023-04-25 15:46:00 +04:00
_ -> do
withAgent ( ` xftpDeleteRcvFile ` aFileId )
throwChatError $ CENoRcvFileUser $ AgentRcvFileId aFileId
2023-03-13 10:30:32 +00:00
where
process :: User -> m ()
process user = do
2023-04-18 12:48:36 +04:00
ft @ RcvFileTransfer { fileId } <- withStore $ \ db -> do
2023-03-30 14:10:13 +04:00
fileId <- getXFTPRcvFileDBId db $ AgentRcvFileId aFileId
getRcvFileTransfer db user fileId
2023-04-18 12:48:36 +04:00
unless ( rcvFileCompleteOrCancelled ft ) $ case msg of
RFPROG rcvProgress rcvTotal -> do
let status = CIFSRcvTransfer { rcvProgress , rcvTotal }
ci <- withStore $ \ db -> do
liftIO $ updateCIFileStatus db user fileId status
getChatItemByFileId db user fileId
toView $ CRRcvFileProgressXFTP user ci rcvProgress rcvTotal
RFDONE xftpPath ->
case liveRcvFileTransferPath ft of
Nothing -> throwChatError $ CEInternalError " no target path for received XFTP file "
Just targetPath -> do
fsTargetPath <- toFSFilePath targetPath
renameFile xftpPath fsTargetPath
ci <- withStore $ \ db -> do
liftIO $ do
updateRcvFileStatus db fileId FSComplete
updateCIFileStatus db user fileId CIFSRcvComplete
getChatItemByFileId db user fileId
2023-04-25 15:46:00 +04:00
agentXFTPDeleteRcvFile aFileId fileId
2023-04-18 12:48:36 +04:00
toView $ CRRcvFileComplete user ci
RFERR e
| temporaryAgentError e ->
throwChatError $ CEXFTPRcvFile fileId ( AgentRcvFileId aFileId ) e
| otherwise -> do
2023-03-30 14:10:13 +04:00
ci <- withStore $ \ db -> do
2023-04-18 12:48:36 +04:00
liftIO $ updateFileCancelled db user fileId CIFSRcvError
2023-03-30 14:10:13 +04:00
getChatItemByFileId db user fileId
2023-04-25 15:46:00 +04:00
agentXFTPDeleteRcvFile aFileId fileId
2023-09-13 15:28:46 +04:00
toView $ CRRcvFileError user ci e
2023-03-13 10:30:32 +00:00
processAgentMessageConn :: forall m . ChatMonad m => User -> ACorrId -> ConnId -> ACommand 'Agent 'AEConn -> m ()
2023-01-04 21:06:28 +04:00
processAgentMessageConn user _ agentConnId END =
2022-10-18 00:35:29 +04:00
withStore ( \ db -> getConnectionEntity db user $ AgentConnId agentConnId ) >>= \ case
RcvDirectMsgConnection _ ( Just ct @ Contact { localDisplayName = c } ) -> do
2023-01-04 21:06:28 +04:00
toView $ CRContactAnotherClient user ct
2023-03-22 15:58:01 +00:00
whenUserNtfs user $ showToast ( c <> " > " ) " connected to another client "
2022-10-18 00:35:29 +04:00
unsetActive $ ActiveC c
2023-01-04 21:06:28 +04:00
entity -> toView $ CRSubscriptionEnd user entity
2023-01-09 17:02:38 +04:00
processAgentMessageConn user @ User { userId } corrId agentConnId agentMessage = do
2023-01-06 13:11:21 +04:00
entity <- withStore ( \ db -> getConnectionEntity db user $ AgentConnId agentConnId ) >>= updateConnStatus
case entity of
2022-01-26 16:18:27 +04:00
RcvDirectMsgConnection conn contact_ ->
2023-01-06 13:11:21 +04:00
processDirectMessage agentMessage entity conn contact_
2022-01-26 16:18:27 +04:00
RcvGroupMsgConnection conn gInfo m ->
2023-01-06 13:11:21 +04:00
processGroupMessage agentMessage entity conn gInfo m
2021-09-04 07:32:56 +01:00
RcvFileConnection conn ft ->
2023-01-06 13:11:21 +04:00
processRcvFileConn agentMessage entity conn ft
2021-09-04 07:32:56 +01:00
SndFileConnection conn ft ->
2023-01-06 13:11:21 +04:00
processSndFileConn agentMessage entity conn ft
2021-12-08 13:09:51 +00:00
UserContactConnection conn uc ->
2023-01-06 13:11:21 +04:00
processUserContactRequest agentMessage entity conn uc
2021-07-24 18:11:04 +01:00
where
2022-02-02 11:31:01 +00:00
updateConnStatus :: ConnectionEntity -> m ConnectionEntity
updateConnStatus acEntity = case agentMsgConnStatus agentMessage of
Just connStatus -> do
let conn = ( entityConnection acEntity ) { connStatus }
2022-06-18 20:06:13 +01:00
withStore' $ \ db -> updateConnectionStatus db conn connStatus
2022-02-02 17:01:12 +00:00
pure $ updateEntityConnStatus acEntity connStatus
2022-02-02 11:31:01 +00:00
Nothing -> pure acEntity
2022-01-26 16:18:27 +04:00
isMember :: MemberId -> GroupInfo -> [ GroupMember ] -> Bool
isMember memId GroupInfo { membership } members =
2022-01-11 08:50:44 +00:00
sameMemberId memId membership || isJust ( find ( sameMemberId memId ) members )
2021-07-24 18:11:04 +01:00
2023-03-10 17:23:04 +00:00
agentMsgConnStatus :: ACommand 'Agent e -> Maybe ConnStatus
2021-07-24 18:11:04 +01:00
agentMsgConnStatus = \ case
2021-12-08 13:09:51 +00:00
CONF { } -> Just ConnRequested
2021-07-24 18:11:04 +01:00
INFO _ -> Just ConnSndReady
CON -> Just ConnReady
_ -> Nothing
2023-03-10 17:23:04 +00:00
processDirectMessage :: ACommand 'Agent e -> ConnectionEntity -> Connection -> Maybe Contact -> m ()
2023-09-06 11:41:23 +04:00
processDirectMessage agentMsg connEntity conn @ Connection { connId , peerChatVRange , viaUserContactLink , groupLinkId , customUserProfileId , connectionCode } = \ case
2021-07-24 18:11:04 +01:00
Nothing -> case agentMsg of
2022-07-20 14:57:16 +01:00
CONF confId _ connInfo -> do
2022-08-18 11:35:31 +04:00
-- [incognito] send saved profile
incognitoProfile <- forM customUserProfileId $ \ profileId -> withStore ( \ db -> getProfileById db userId profileId )
2022-11-01 17:32:49 +03:00
let profileToSend = userProfileToSend user ( fromLocalProfile <$> incognitoProfile ) Nothing
2023-09-01 19:20:07 +04:00
conn' <- saveConnInfo conn connInfo
2022-09-14 19:45:21 +04:00
-- [async agent commands] no continuation needed, but command should be asynchronous for stability
2023-09-01 19:20:07 +04:00
allowAgentConnectionAsync user conn' confId $ XInfo profileToSend
INFO connInfo -> do
_conn' <- saveConnInfo conn connInfo
pure ()
2022-06-07 14:14:54 +01:00
MSG meta _msgFlags msgBody -> do
2022-09-14 19:45:21 +04:00
cmdId <- createAckCmd conn
2023-09-01 19:20:07 +04:00
withAckMessage agentConnId cmdId meta $ do
( _conn' , _ ) <- saveRcvMSG conn ( ConnectionId connId ) meta msgBody cmdId
pure False
2021-12-29 23:11:55 +04:00
SENT msgId ->
sentMsgDeliveryEvent conn msgId
2022-09-14 19:45:21 +04:00
OK ->
-- [async agent commands] continuation on receiving OK
withCompletedCommand conn agentMsg $ \ CommandData { cmdFunction , cmdId } ->
when ( cmdFunction == CFAckMessage ) $ ackMsgDeliveryEvent conn cmdId
2023-01-07 19:47:51 +04:00
MERR _ err -> do
2023-01-09 17:02:38 +04:00
toView $ CRChatError ( Just user ) ( ChatErrorAgent err $ Just connEntity )
2023-01-07 19:47:51 +04:00
incAuthErrCounter connEntity conn err
2022-09-16 19:30:02 +04:00
ERR err -> do
2023-01-09 17:02:38 +04:00
toView $ CRChatError ( Just user ) ( ChatErrorAgent err $ Just connEntity )
2022-09-16 19:30:02 +04:00
when ( corrId /= " " ) $ withCompletedCommand conn agentMsg $ \ _cmdData -> pure ()
2022-01-12 11:54:40 +00:00
-- TODO add debugging output
2021-08-14 21:04:51 +01:00
_ -> pure ()
2022-03-13 19:34:03 +00:00
Just ct @ Contact { localDisplayName = c , contactId } -> case agentMsg of
2022-09-14 19:45:21 +04:00
INV ( ACR _ cReq ) ->
-- [async agent commands] XGrpMemIntro continuation on receiving INV
withCompletedCommand conn agentMsg $ \ _ ->
case cReq of
directConnReq @ ( CRInvitationUri _ _ ) -> do
contData <- withStore' $ \ db -> do
setConnConnReqInv db user connId cReq
getXGrpMemIntroContDirect db user ct
forM_ contData $ \ ( hostConnId , xGrpMemIntroCont ) ->
2023-09-05 20:15:50 +04:00
sendXGrpMemInv hostConnId ( Just directConnReq ) xGrpMemIntroCont
2022-09-14 19:45:21 +04:00
CRContactUri _ -> throwChatError $ CECommandError " unexpected ConnectionRequestUri type "
2022-06-07 14:14:54 +01:00
MSG msgMeta _msgFlags msgBody -> do
2022-09-14 19:45:21 +04:00
cmdId <- createAckCmd conn
2022-12-03 18:06:21 +00:00
withAckMessage agentConnId cmdId msgMeta $ do
2023-09-01 19:20:07 +04:00
( conn' , msg @ RcvMessage { chatMsgEvent = ACME _ event } ) <- saveRcvMSG conn ( ConnectionId connId ) msgMeta msgBody cmdId
let ct' = ct { activeConn = conn' } :: Contact
assertDirectAllowed user MDRcv ct' $ toCMEventTag event
2022-12-03 18:06:21 +00:00
updateChatLock " directMessage " event
2022-10-14 13:06:33 +01:00
case event of
2023-09-01 19:20:07 +04:00
XMsgNew mc -> newContentMessage ct' mc msg msgMeta
XMsgFileDescr sharedMsgId fileDescr -> messageFileDescription ct' sharedMsgId fileDescr msgMeta
XMsgFileCancel sharedMsgId -> cancelMessageFile ct' sharedMsgId msgMeta
XMsgUpdate sharedMsgId mContent ttl live -> messageUpdate ct' sharedMsgId mContent msg msgMeta ttl live
XMsgDel sharedMsgId _ -> messageDelete ct' sharedMsgId msg msgMeta
XMsgReact sharedMsgId _ reaction add -> directMsgReaction ct' sharedMsgId reaction add msg msgMeta
2022-04-10 13:30:58 +04:00
-- TODO discontinue XFile
2023-09-01 19:20:07 +04:00
XFile fInv -> processFileInvitation' ct' fInv msg msgMeta
XFileCancel sharedMsgId -> xFileCancel ct' sharedMsgId msgMeta
XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInv ct' sharedMsgId fileConnReq_ fName msgMeta
XInfo p -> xInfo ct' p
XGrpInv gInv -> processGroupInvitation ct' gInv msg msgMeta
2023-09-20 00:26:03 +04:00
XInfoProbe probe -> xInfoProbe ( CGMContact ct' ) probe
2023-09-01 19:20:07 +04:00
XInfoProbeCheck probeHash -> xInfoProbeCheck ct' probeHash
XInfoProbeOk probe -> xInfoProbeOk ct' probe
XCallInv callId invitation -> xCallInv ct' callId invitation msg msgMeta
XCallOffer callId offer -> xCallOffer ct' callId offer msg msgMeta
XCallAnswer callId answer -> xCallAnswer ct' callId answer msg msgMeta
XCallExtra callId extraInfo -> xCallExtra ct' callId extraInfo msg msgMeta
XCallEnd callId -> xCallEnd ct' callId msg msgMeta
BFileChunk sharedMsgId chunk -> bFileChunk ct' sharedMsgId chunk msgMeta
2022-10-14 13:06:33 +01:00
_ -> messageError $ " unsupported message: " <> T . pack ( show event )
2023-09-01 19:20:07 +04:00
let Contact { chatSettings = ChatSettings { sendRcpts } } = ct'
2023-07-13 23:48:25 +01:00
pure $ fromMaybe ( sendRcptsContacts user ) sendRcpts && hasDeliveryReceipt ( toCMEventTag event )
RCVD msgMeta msgRcpt ->
withAckMessage' agentConnId conn msgMeta $
directMsgReceived ct conn msgMeta msgRcpt
2022-07-20 14:57:16 +01:00
CONF confId _ connInfo -> do
2023-09-01 19:20:07 +04:00
ChatMessage { chatVRange , chatMsgEvent } <- parseChatMessage conn connInfo
2023-09-06 11:41:23 +04:00
conn' <- updatePeerChatVRange conn chatVRange
2021-07-24 10:26:28 +01:00
case chatMsgEvent of
2023-09-16 17:55:48 +04:00
-- confirming direct connection with a member
2021-07-24 10:26:28 +01:00
XGrpMemInfo _memId _memProfile -> do
-- TODO check member ID
-- TODO update member profile
2022-09-14 19:45:21 +04:00
-- [async agent commands] no continuation needed, but command should be asynchronous for stability
2023-09-01 19:20:07 +04:00
allowAgentConnectionAsync user conn' confId XOk
2023-09-16 17:55:48 +04:00
XOk -> do
allowAgentConnectionAsync user conn' confId XOk
void $ withStore' $ \ db -> resetMemberContactFields db ct
_ -> messageError " CONF for existing contact must have x.grp.mem.info or x.ok "
2021-07-24 10:26:28 +01:00
INFO connInfo -> do
2023-09-01 19:20:07 +04:00
ChatMessage { chatVRange , chatMsgEvent } <- parseChatMessage conn connInfo
2023-09-06 11:41:23 +04:00
_conn' <- updatePeerChatVRange conn chatVRange
2021-07-24 10:26:28 +01:00
case chatMsgEvent of
XGrpMemInfo _memId _memProfile -> do
-- TODO check member ID
-- TODO update member profile
pure ()
2021-12-08 13:09:51 +00:00
XInfo _profile -> do
-- TODO update contact profile
pure ()
2021-07-24 10:26:28 +01:00
XOk -> pure ()
2021-12-13 12:05:57 +00:00
_ -> messageError " INFO for existing contact must have x.grp.mem.info, x.info or x.ok "
2021-07-24 18:11:04 +01:00
CON ->
2022-06-18 20:06:13 +01:00
withStore' ( \ db -> getViaGroupMember db user ct ) >>= \ case
2021-07-24 10:26:28 +01:00
Nothing -> do
2022-08-18 11:35:31 +04:00
-- [incognito] print incognito profile used for this contact
incognitoProfile <- forM customUserProfileId $ \ profileId -> withStore ( \ db -> getProfileById db userId profileId )
2023-01-04 21:06:28 +04:00
toView $ CRContactConnected user ct ( fmap fromLocalProfile incognitoProfile )
2022-12-12 15:27:52 +04:00
when ( directOrUsed ct ) $ createFeatureEnabledItems ct
2023-03-22 15:58:01 +00:00
whenUserNtfs user $ do
setActive $ ActiveC c
showToast ( c <> " > " ) " connected "
2023-05-26 13:52:06 +04:00
forM_ groupLinkId $ \ _ -> probeMatchingContacts ct $ contactConnIncognito ct
2022-10-13 17:12:22 +04:00
forM_ viaUserContactLink $ \ userContactLinkId ->
2022-06-27 19:41:25 +01:00
withStore' ( \ db -> getUserContactLinkById db userId userContactLinkId ) >>= \ case
2023-03-06 09:51:42 +00:00
Just ( UserContactLink { autoAccept = Just AutoAccept { autoReply = mc_ } } , groupId_ , gLinkMemRole ) -> do
2022-10-13 17:12:22 +04:00
forM_ mc_ $ \ mc -> do
2022-12-16 07:51:04 +00:00
( msg , _ ) <- sendDirectContactMessage ct ( XMsgNew $ MCSimple ( extMsgContent mc Nothing ) )
ci <- saveSndChatItem user ( CDDirectSnd ct ) msg ( CISndMsgContent mc )
2023-01-04 21:06:28 +04:00
toView $ CRNewChatItem user ( AChatItem SCTDirect SMDSnd ( DirectChat ct ) ci )
2022-10-13 17:12:22 +04:00
forM_ groupId_ $ \ groupId -> do
2023-09-10 22:40:15 +03:00
subMode <- chatReadVar subscriptionMode
2022-10-13 17:12:22 +04:00
gVar <- asks idsDrg
2023-09-10 22:40:15 +03:00
groupConnIds <- createAgentConnectionAsync user CFCreateConnGrpInv True SCMInvitation subMode
2023-09-12 17:59:09 +04:00
withStore $ \ db -> createNewContactMemberAsync db gVar user groupId ct gLinkMemRole groupConnIds ( fromJVersionRange peerChatVRange ) subMode
2022-06-27 19:41:25 +01:00
_ -> pure ()
2023-09-19 18:50:10 +01:00
Just ( gInfo , m @ GroupMember { activeConn } ) ->
2022-02-14 18:49:42 +04:00
when ( maybe False ( ( == ConnReady ) . connStatus ) activeConn ) $ do
2023-06-09 16:43:53 +04:00
notifyMemberConnected gInfo m $ Just ct
2023-09-19 18:50:10 +01:00
let connectedIncognito = contactConnIncognito ct || incognitoMembership gInfo
2022-08-27 19:56:03 +04:00
when ( memberCategory m == GCPreMember ) $ probeMatchingContacts ct connectedIncognito
2022-02-07 15:19:34 +04:00
SENT msgId -> do
2021-12-29 23:11:55 +04:00
sentMsgDeliveryEvent conn msgId
2022-10-14 13:06:33 +01:00
checkSndInlineFTComplete conn msgId
2023-07-26 14:49:35 +04:00
updateDirectItemStatus ct conn msgId $ CISSndSent SSPComplete
2022-11-01 13:26:08 +00:00
SWITCH qd phase cStats -> do
2023-01-04 21:06:28 +04:00
toView $ CRContactSwitch user ct ( SwitchProgress qd phase cStats )
2023-06-16 19:05:53 +04:00
when ( phase ` elem ` [ SPStarted , SPCompleted ] ) $ case qd of
2022-11-23 11:04:08 +00:00
QDRcv -> createInternalChatItem user ( CDDirectSnd ct ) ( CISndConnEvent $ SCESwitchQueue phase Nothing ) Nothing
QDSnd -> createInternalChatItem user ( CDDirectRcv ct ) ( CIRcvConnEvent $ RCESwitchQueue phase ) Nothing
2023-07-05 19:44:21 +04:00
RSYNC rss cryptoErr_ cStats ->
case ( rss , connectionCode , cryptoErr_ ) of
( RSRequired , _ , Just cryptoErr ) -> processErr cryptoErr
( RSAllowed , _ , Just cryptoErr ) -> processErr cryptoErr
( RSAgreed , Just _ , _ ) -> do
withStore' $ \ db -> setConnectionVerified db user connId Nothing
let ct' = ct { activeConn = conn { connectionCode = Nothing } } :: Contact
ratchetSyncEventItem ct'
toView $ CRContactVerificationReset user ct'
createInternalChatItem user ( CDDirectRcv ct' ) ( CIRcvConnEvent RCEVerificationCodeReset ) Nothing
_ -> ratchetSyncEventItem ct
where
processErr cryptoErr = do
let e @ ( mde , n ) = agentMsgDecryptError cryptoErr
ci_ <- withStore $ \ db ->
getDirectChatItemsLast db user contactId 1 " "
>>= liftIO
. mapM ( \ ( ci , content' ) -> updateDirectChatItem' db user contactId ci content' False Nothing )
. ( mdeUpdatedCI e <=< headMaybe )
case ci_ of
Just ci -> toView $ CRChatItemUpdated user ( AChatItem SCTDirect SMDRcv ( DirectChat ct ) ci )
_ -> do
toView $ CRContactRatchetSync user ct ( RatchetSyncProgress rss cStats )
createInternalChatItem user ( CDDirectRcv ct ) ( CIRcvDecryptionError mde n ) Nothing
headMaybe = \ case
x : _ -> Just x
_ -> Nothing
ratchetSyncEventItem ct' = do
toView $ CRContactRatchetSync user ct' ( RatchetSyncProgress rss cStats )
createInternalChatItem user ( CDDirectRcv ct' ) ( CIRcvConnEvent $ RCERatchetSync rss ) Nothing
2022-09-14 19:45:21 +04:00
OK ->
-- [async agent commands] continuation on receiving OK
withCompletedCommand conn agentMsg $ \ CommandData { cmdFunction , cmdId } ->
when ( cmdFunction == CFAckMessage ) $ ackMsgDeliveryEvent conn cmdId
2022-02-07 15:19:34 +04:00
MERR msgId err -> do
2023-07-26 14:49:35 +04:00
updateDirectItemStatus ct conn msgId $ agentErrToItemStatus err
2023-01-09 17:02:38 +04:00
toView $ CRChatError ( Just user ) ( ChatErrorAgent err $ Just connEntity )
2023-01-07 19:47:51 +04:00
incAuthErrCounter connEntity conn err
2022-09-16 19:30:02 +04:00
ERR err -> do
2023-01-09 17:02:38 +04:00
toView $ CRChatError ( Just user ) ( ChatErrorAgent err $ Just connEntity )
2022-09-16 19:30:02 +04:00
when ( corrId /= " " ) $ withCompletedCommand conn agentMsg $ \ _cmdData -> pure ()
2022-01-12 11:54:40 +00:00
-- TODO add debugging output
2021-08-14 21:04:51 +01:00
_ -> pure ()
2021-07-24 10:26:28 +01:00
2023-03-10 17:23:04 +00:00
processGroupMessage :: ACommand 'Agent e -> ConnectionEntity -> Connection -> GroupInfo -> GroupMember -> m ()
2023-07-05 19:44:21 +04:00
processGroupMessage agentMsg connEntity conn @ Connection { connId , connectionCode } gInfo @ GroupInfo { groupId , localDisplayName = gName , groupProfile , membership , chatSettings } m = case agentMsg of
2022-09-14 19:45:21 +04:00
INV ( ACR _ cReq ) ->
2022-10-15 14:48:07 +04:00
withCompletedCommand conn agentMsg $ \ CommandData { cmdFunction } ->
2022-09-14 19:45:21 +04:00
case cReq of
2022-10-15 14:48:07 +04:00
groupConnReq @ ( CRInvitationUri _ _ ) -> case cmdFunction of
-- [async agent commands] XGrpMemIntro continuation on receiving INV
2023-09-06 17:48:37 +04:00
CFCreateConnGrpMemInv
2023-09-20 00:26:03 +04:00
| isCompatibleRange ( fromJVersionRange $ peerChatVRange conn ) groupNoDirectVRange -> sendWithoutDirectCReq
2023-09-06 17:48:37 +04:00
| otherwise -> sendWithDirectCReq
2023-09-05 20:15:50 +04:00
where
sendWithoutDirectCReq = do
let GroupMember { groupMemberId , memberId } = m
hostConnId <- withStore $ \ db -> do
liftIO $ setConnConnReqInv db user connId cReq
getHostConnId db user groupId
sendXGrpMemInv hostConnId Nothing XGrpMemIntroCont { groupId , groupMemberId , memberId , groupConnReq }
sendWithDirectCReq = do
let GroupMember { groupMemberId , memberId } = m
contData <- withStore' $ \ db -> do
setConnConnReqInv db user connId cReq
getXGrpMemIntroContGroup db user m
forM_ contData $ \ ( hostConnId , directConnReq ) ->
sendXGrpMemInv hostConnId ( Just directConnReq ) XGrpMemIntroCont { groupId , groupMemberId , memberId , groupConnReq }
2022-10-15 14:48:07 +04:00
-- [async agent commands] group link auto-accept continuation on receiving INV
2023-09-05 20:15:50 +04:00
CFCreateConnGrpInv -> do
ct <- withStore $ \ db -> getContactViaMember db user m
withStore' $ \ db -> setNewContactMemberConnRequest db user m cReq
groupLinkId <- withStore' $ \ db -> getGroupLinkId db user gInfo
sendGrpInvitation ct m groupLinkId
toView $ CRSentGroupInvitation user gInfo ct m
2022-10-21 17:35:07 +04:00
where
2022-11-03 14:46:36 +04:00
sendGrpInvitation :: Contact -> GroupMember -> Maybe GroupLinkId -> m ()
sendGrpInvitation ct GroupMember { memberId , memberRole = memRole } groupLinkId = do
2022-10-21 17:35:07 +04:00
let GroupMember { memberRole = userRole , memberId = userMemberId } = membership
2022-11-03 14:46:36 +04:00
groupInv = GroupInvitation ( MemberIdRole userMemberId userRole ) ( MemberIdRole memberId memRole ) cReq groupProfile groupLinkId
2022-10-21 17:35:07 +04:00
( _msg , _ ) <- sendDirectContactMessage ct $ XGrpInv groupInv
-- we could link chat item with sent group invitation message (_msg)
2022-11-23 11:04:08 +00:00
createInternalChatItem user ( CDGroupRcv gInfo m ) ( CIRcvGroupEvent RGEInvitedViaGroupLink ) Nothing
2022-10-15 14:48:07 +04:00
_ -> throwChatError $ CECommandError " unexpected cmdFunction "
2022-09-14 19:45:21 +04:00
CRContactUri _ -> throwChatError $ CECommandError " unexpected ConnectionRequestUri type "
2022-07-20 14:57:16 +01:00
CONF confId _ connInfo -> do
2023-09-01 19:20:07 +04:00
ChatMessage { chatVRange , chatMsgEvent } <- parseChatMessage conn connInfo
2023-09-06 11:41:23 +04:00
conn' <- updatePeerChatVRange conn chatVRange
2021-07-24 18:11:04 +01:00
case memberCategory m of
GCInviteeMember ->
case chatMsgEvent of
2022-08-27 19:56:03 +04:00
XGrpAcpt memId
2022-01-11 08:50:44 +00:00
| sameMemberId memId m -> do
2022-10-15 14:48:07 +04:00
withStore $ \ db -> liftIO $ updateGroupMemberStatus db userId m GSMemAccepted
-- [async agent commands] no continuation needed, but command should be asynchronous for stability
2023-09-01 19:20:07 +04:00
allowAgentConnectionAsync user conn' confId XOk
2021-07-24 18:11:04 +01:00
| otherwise -> messageError " x.grp.acpt: memberId is different from expected "
2021-12-08 13:09:51 +00:00
_ -> messageError " CONF from invited member must have x.grp.acpt "
2021-07-24 18:11:04 +01:00
_ ->
case chatMsgEvent of
XGrpMemInfo memId _memProfile
2022-01-11 08:50:44 +00:00
| sameMemberId memId m -> do
2022-10-15 14:48:07 +04:00
-- TODO update member profile
-- [async agent commands] no continuation needed, but command should be asynchronous for stability
2023-09-10 21:11:35 +01:00
allowAgentConnectionAsync user conn' confId $ XGrpMemInfo membership . memberId ( fromLocalProfile $ memberProfile membership )
2021-07-24 18:11:04 +01:00
| otherwise -> messageError " x.grp.mem.info: memberId is different from expected "
2021-12-08 13:09:51 +00:00
_ -> messageError " CONF from member must have x.grp.mem.info "
2021-07-24 18:11:04 +01:00
INFO connInfo -> do
2023-09-01 19:20:07 +04:00
ChatMessage { chatVRange , chatMsgEvent } <- parseChatMessage conn connInfo
2023-09-06 11:41:23 +04:00
_conn' <- updatePeerChatVRange conn chatVRange
2021-07-24 18:11:04 +01:00
case chatMsgEvent of
XGrpMemInfo memId _memProfile
2022-01-11 08:50:44 +00:00
| sameMemberId memId m -> do
2022-10-15 14:48:07 +04:00
-- TODO update member profile
pure ()
2021-07-24 18:11:04 +01:00
| otherwise -> messageError " x.grp.mem.info: memberId is different from expected "
XOk -> pure ()
_ -> messageError " INFO from member must have x.grp.mem.info "
pure ()
CON -> do
2022-06-18 20:06:13 +01:00
members <- withStore' $ \ db -> getGroupMembers db user gInfo
withStore' $ \ db -> do
updateGroupMemberStatus db userId m GSMemConnected
2021-07-27 08:08:05 +01:00
unless ( memberActive membership ) $
2022-06-18 20:06:13 +01:00
updateGroupMemberStatus db userId membership GSMemConnected
2023-01-07 19:47:51 +04:00
-- possible improvement: check for each pending message, requires keeping track of connection state
2023-01-13 14:19:21 +04:00
unless ( connDisabled conn ) $ sendPendingGroupMessages user m conn
2022-09-26 18:09:45 +01:00
withAgent $ \ a -> toggleConnectionNtfs a ( aConnId conn ) $ enableNtfs chatSettings
2021-07-24 18:11:04 +01:00
case memberCategory m of
GCHostMember -> do
2023-01-04 21:06:28 +04:00
toView $ CRUserJoinedGroup user gInfo { membership = membership { memberStatus = GSMemConnected } } m { memberStatus = GSMemConnected }
2022-11-23 11:04:08 +00:00
createGroupFeatureItems gInfo m
2022-12-10 08:27:32 +00:00
let GroupInfo { groupProfile = GroupProfile { description } } = gInfo
2022-11-23 11:04:08 +00:00
memberConnectedChatItem gInfo m
2022-12-10 08:27:32 +00:00
forM_ description $ groupDescriptionChatItem gInfo m
2023-03-22 15:58:01 +00:00
whenUserNtfs user $ do
setActive $ ActiveG gName
showToast ( " # " <> gName ) " you are connected to group "
2021-07-24 18:11:04 +01:00
GCInviteeMember -> do
2022-08-27 19:56:03 +04:00
memberConnectedChatItem gInfo m
2023-01-04 21:06:28 +04:00
toView $ CRJoinedGroupMember user gInfo m { memberStatus = GSMemConnected }
2023-03-22 15:58:01 +00:00
whenGroupNtfs user gInfo $ do
setActive $ ActiveG gName
2023-08-25 04:56:37 +08:00
showToast ( " # " <> gName ) $ " member " <> m . localDisplayName <> " is connected "
2022-06-18 20:06:13 +01:00
intros <- withStore' $ \ db -> createIntroductions db members m
2023-01-13 14:19:21 +04:00
void . sendGroupMessage user gInfo members . XGrpMemNew $ memberInfo m
2023-01-12 16:31:27 +04:00
forM_ intros $ \ intro ->
2023-07-09 23:24:38 +01:00
processIntro intro ` catchChatError ` ( toView . CRChatError ( Just user ) )
2023-01-12 16:31:27 +04:00
where
processIntro intro @ GroupMemberIntro { introId } = do
2023-09-05 20:15:50 +04:00
void $ sendDirectMessage conn ( XGrpMemIntro $ memberInfo ( reMember intro ) ) ( GroupId groupId )
2023-01-12 16:31:27 +04:00
withStore' $ \ db -> updateIntroStatus db introId GMIntroSent
2021-07-24 18:11:04 +01:00
_ -> do
-- TODO notify member who forwarded introduction - question - where it is stored? There is via_contact but probably there should be via_member in group_members table
2022-06-18 20:06:13 +01:00
withStore' ( \ db -> getViaGroupContact db user m ) >>= \ case
2021-07-24 18:11:04 +01:00
Nothing -> do
2023-06-09 16:43:53 +04:00
notifyMemberConnected gInfo m Nothing
2023-09-20 00:26:03 +04:00
let connectedIncognito = memberIncognito membership
when ( memberCategory m == GCPreMember ) $ probeMatchingMemberContact gInfo m connectedIncognito
2022-02-14 18:49:42 +04:00
Just ct @ Contact { activeConn = Connection { connStatus } } ->
when ( connStatus == ConnReady ) $ do
2023-06-09 16:43:53 +04:00
notifyMemberConnected gInfo m $ Just ct
2023-09-19 18:50:10 +01:00
let connectedIncognito = contactConnIncognito ct || incognitoMembership gInfo
2022-08-27 19:56:03 +04:00
when ( memberCategory m == GCPreMember ) $ probeMatchingContacts ct connectedIncognito
2022-06-07 14:14:54 +01:00
MSG msgMeta _msgFlags msgBody -> do
2022-09-14 19:45:21 +04:00
cmdId <- createAckCmd conn
2023-02-04 12:25:11 +00:00
withAckMessage agentConnId cmdId msgMeta $ do
2023-09-01 19:20:07 +04:00
( conn' , msg @ RcvMessage { chatMsgEvent = ACME _ event } ) <- saveRcvMSG conn ( GroupId groupId ) msgMeta msgBody cmdId
let m' = m { activeConn = Just conn' } :: GroupMember
2023-02-04 12:25:11 +00:00
updateChatLock " groupMessage " event
2022-10-14 13:06:33 +01:00
case event of
2023-09-01 19:20:07 +04:00
XMsgNew mc -> canSend m' $ newGroupContentMessage gInfo m' mc msg msgMeta
XMsgFileDescr sharedMsgId fileDescr -> canSend m' $ groupMessageFileDescription gInfo m' sharedMsgId fileDescr msgMeta
XMsgFileCancel sharedMsgId -> cancelGroupMessageFile gInfo m' sharedMsgId msgMeta
XMsgUpdate sharedMsgId mContent ttl live -> canSend m' $ groupMessageUpdate gInfo m' sharedMsgId mContent msg msgMeta ttl live
XMsgDel sharedMsgId memberId -> groupMessageDelete gInfo m' sharedMsgId memberId msg msgMeta
XMsgReact sharedMsgId ( Just memberId ) reaction add -> groupMsgReaction gInfo m' sharedMsgId memberId reaction add msg msgMeta
2022-04-10 13:30:58 +04:00
-- TODO discontinue XFile
2023-09-01 19:20:07 +04:00
XFile fInv -> processGroupFileInvitation' gInfo m' fInv msg msgMeta
XFileCancel sharedMsgId -> xFileCancelGroup gInfo m' sharedMsgId msgMeta
XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInvGroup gInfo m' sharedMsgId fileConnReq_ fName msgMeta
XGrpMemNew memInfo -> xGrpMemNew gInfo m' memInfo msg msgMeta
XGrpMemIntro memInfo -> xGrpMemIntro gInfo m' memInfo
XGrpMemInv memId introInv -> xGrpMemInv gInfo m' memId introInv
XGrpMemFwd memInfo introInv -> xGrpMemFwd gInfo m' memInfo introInv
XGrpMemRole memId memRole -> xGrpMemRole gInfo m' memId memRole msg msgMeta
XGrpMemDel memId -> xGrpMemDel gInfo m' memId msg msgMeta
XGrpLeave -> xGrpLeave gInfo m' msg msgMeta
XGrpDel -> xGrpDel gInfo m' msg msgMeta
XGrpInfo p' -> xGrpInfo gInfo m' p' msg msgMeta
2023-09-16 17:55:48 +04:00
XGrpDirectInv connReq mContent_ -> canSend m' $ xGrpDirectInv gInfo m' conn' connReq mContent_ msg msgMeta
2023-09-20 00:26:03 +04:00
XInfoProbe probe -> xInfoProbe ( CGMGroupMember gInfo m' ) probe
-- XInfoProbeCheck -- TODO merge members?
-- XInfoProbeOk -- TODO merge members?
2022-10-14 13:06:33 +01:00
BFileChunk sharedMsgId chunk -> bFileChunkGroup gInfo sharedMsgId chunk msgMeta
_ -> messageError $ " unsupported message: " <> T . pack ( show event )
2023-07-26 14:49:35 +04:00
currentMemCount <- withStore' $ \ db -> getGroupCurrentMembersCount db user gInfo
let GroupInfo { chatSettings = ChatSettings { sendRcpts } } = gInfo
pure $
fromMaybe ( sendRcptsSmallGroups user ) sendRcpts
&& hasDeliveryReceipt ( toCMEventTag event )
&& currentMemCount <= smallGroupsRcptsMemLimit
2023-02-01 13:57:39 +00:00
where
2023-09-10 21:11:35 +01:00
canSend :: GroupMember -> m () -> m ()
2023-09-01 19:20:07 +04:00
canSend mem a
2023-09-10 21:11:35 +01:00
| mem . memberRole <= GRObserver = messageError " member is not allowed to send messages "
2023-02-01 13:57:39 +00:00
| otherwise = a
2023-07-13 23:48:25 +01:00
RCVD msgMeta msgRcpt ->
withAckMessage' agentConnId conn msgMeta $
groupMsgReceived gInfo m conn msgMeta msgRcpt
2022-10-14 13:06:33 +01:00
SENT msgId -> do
2021-12-29 23:11:55 +04:00
sentMsgDeliveryEvent conn msgId
2022-10-14 13:06:33 +01:00
checkSndInlineFTComplete conn msgId
2023-07-26 14:49:35 +04:00
updateGroupItemStatus gInfo m conn msgId $ CISSndSent SSPComplete
2022-11-01 13:26:08 +00:00
SWITCH qd phase cStats -> do
2023-01-04 21:06:28 +04:00
toView $ CRGroupMemberSwitch user gInfo m ( SwitchProgress qd phase cStats )
2023-06-16 19:05:53 +04:00
when ( phase ` elem ` [ SPStarted , SPCompleted ] ) $ case qd of
2022-11-23 11:04:08 +00:00
QDRcv -> createInternalChatItem user ( CDGroupSnd gInfo ) ( CISndConnEvent . SCESwitchQueue phase . Just $ groupMemberRef m ) Nothing
QDSnd -> createInternalChatItem user ( CDGroupRcv gInfo m ) ( CIRcvConnEvent $ RCESwitchQueue phase ) Nothing
2023-07-05 19:44:21 +04:00
RSYNC rss cryptoErr_ cStats ->
case ( rss , connectionCode , cryptoErr_ ) of
( RSRequired , _ , Just cryptoErr ) -> processErr cryptoErr
( RSAllowed , _ , Just cryptoErr ) -> processErr cryptoErr
( RSAgreed , Just _ , _ ) -> do
withStore' $ \ db -> setConnectionVerified db user connId Nothing
let m' = m { activeConn = Just ( conn { connectionCode = Nothing } :: Connection ) } :: GroupMember
ratchetSyncEventItem m'
toView $ CRGroupMemberVerificationReset user gInfo m'
createInternalChatItem user ( CDGroupRcv gInfo m' ) ( CIRcvConnEvent RCEVerificationCodeReset ) Nothing
_ -> ratchetSyncEventItem m
where
processErr cryptoErr = do
let e @ ( mde , n ) = agentMsgDecryptError cryptoErr
ci_ <- withStore $ \ db ->
getGroupMemberChatItemLast db user groupId ( groupMemberId' m )
>>= liftIO
. mapM ( \ ( ci , content' ) -> updateGroupChatItem db user groupId ci content' False Nothing )
. mdeUpdatedCI e
case ci_ of
Just ci -> toView $ CRChatItemUpdated user ( AChatItem SCTGroup SMDRcv ( GroupChat gInfo ) ci )
_ -> do
toView $ CRGroupMemberRatchetSync user gInfo m ( RatchetSyncProgress rss cStats )
createInternalChatItem user ( CDGroupRcv gInfo m ) ( CIRcvDecryptionError mde n ) Nothing
ratchetSyncEventItem m' = do
toView $ CRGroupMemberRatchetSync user gInfo m' ( RatchetSyncProgress rss cStats )
createInternalChatItem user ( CDGroupRcv gInfo m' ) ( CIRcvConnEvent $ RCERatchetSync rss ) Nothing
2022-09-14 19:45:21 +04:00
OK ->
-- [async agent commands] continuation on receiving OK
withCompletedCommand conn agentMsg $ \ CommandData { cmdFunction , cmdId } ->
when ( cmdFunction == CFAckMessage ) $ ackMsgDeliveryEvent conn cmdId
2023-07-26 14:49:35 +04:00
MERR msgId err -> do
chatItemId_ <- withStore' $ \ db -> getChatItemIdByAgentMsgId db connId msgId
forM_ chatItemId_ $ \ itemId -> do
let GroupMember { groupMemberId } = m
updateGroupMemSndStatus itemId groupMemberId $ agentErrToItemStatus err
2023-07-20 16:15:57 +04:00
-- group errors are silenced to reduce load on UI event log
-- toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity)
2023-01-07 19:47:51 +04:00
incAuthErrCounter connEntity conn err
2022-09-16 19:30:02 +04:00
ERR err -> do
2023-01-09 17:02:38 +04:00
toView $ CRChatError ( Just user ) ( ChatErrorAgent err $ Just connEntity )
2022-09-16 19:30:02 +04:00
when ( corrId /= " " ) $ withCompletedCommand conn agentMsg $ \ _cmdData -> pure ()
2022-01-12 11:54:40 +00:00
-- TODO add debugging output
2021-08-14 21:04:51 +01:00
_ -> pure ()
2021-07-24 10:26:28 +01:00
2023-07-05 19:44:21 +04:00
agentMsgDecryptError :: AgentCryptoError -> ( MsgDecryptError , Word32 )
2023-05-17 16:13:35 +04:00
agentMsgDecryptError = \ case
2023-07-05 19:44:21 +04:00
DECRYPT_AES -> ( MDEOther , 1 )
DECRYPT_CB -> ( MDEOther , 1 )
RATCHET_HEADER -> ( MDERatchetHeader , 1 )
RATCHET_EARLIER _ -> ( MDERatchetEarlier , 1 )
RATCHET_SKIPPED n -> ( MDETooManySkipped , n )
2023-04-16 12:35:45 +02:00
mdeUpdatedCI :: ( MsgDecryptError , Word32 ) -> CChatItem c -> Maybe ( ChatItem c 'MDRcv , CIContent 'MDRcv )
mdeUpdatedCI ( mde' , n' ) ( CChatItem _ ci @ ChatItem { content = CIRcvDecryptionError mde n } )
| mde == mde' = case mde of
MDERatchetHeader -> r ( n + n' )
MDETooManySkipped -> r n' -- the numbers are not added as sequential MDETooManySkipped will have it incremented by 1
2023-07-05 19:44:21 +04:00
MDERatchetEarlier -> r ( n + n' )
MDEOther -> r ( n + n' )
2023-04-16 12:35:45 +02:00
| otherwise = Nothing
where
r n'' = Just ( ci , CIRcvDecryptionError mde n'' )
mdeUpdatedCI _ _ = Nothing
2023-03-10 17:23:04 +00:00
processSndFileConn :: ACommand 'Agent e -> ConnectionEntity -> Connection -> SndFileTransfer -> m ()
2023-01-06 13:11:21 +04:00
processSndFileConn agentMsg connEntity conn ft @ SndFileTransfer { fileId , fileName , fileStatus } =
2021-09-04 07:32:56 +01:00
case agentMsg of
2022-05-11 16:18:28 +04:00
-- SMP CONF for SndFileConnection happens for direct file protocol
-- when recipient of the file "joins" connection created by the sender
2022-07-20 14:57:16 +01:00
CONF confId _ connInfo -> do
2023-09-01 19:20:07 +04:00
ChatMessage { chatVRange , chatMsgEvent } <- parseChatMessage conn connInfo
2023-09-06 11:41:23 +04:00
conn' <- updatePeerChatVRange conn chatVRange
2021-09-04 07:32:56 +01:00
case chatMsgEvent of
2021-12-29 23:11:55 +04:00
-- TODO save XFileAcpt message
2021-09-04 07:32:56 +01:00
XFileAcpt name
| name == fileName -> do
2022-10-15 14:48:07 +04:00
withStore' $ \ db -> updateSndFileStatus db ft FSAccepted
-- [async agent commands] no continuation needed, but command should be asynchronous for stability
2023-09-01 19:20:07 +04:00
allowAgentConnectionAsync user conn' confId XOk
2021-09-04 07:32:56 +01:00
| otherwise -> messageError " x.file.acpt: fileName is different from expected "
2021-12-08 13:09:51 +00:00
_ -> messageError " CONF from file connection must have x.file.acpt "
2021-09-04 07:32:56 +01:00
CON -> do
2022-06-18 20:06:13 +01:00
ci <- withStore $ \ db -> do
liftIO $ updateSndFileStatus db ft FSConnected
2023-03-13 10:30:32 +00:00
updateDirectCIFileStatus db user fileId $ CIFSSndTransfer 0 1
2023-01-04 21:06:28 +04:00
toView $ CRSndFileStart user ci ft
2022-05-05 10:37:53 +01:00
sendFileChunk user ft
2021-09-04 07:32:56 +01:00
SENT msgId -> do
2022-06-18 20:06:13 +01:00
withStore' $ \ db -> updateSndFileChunkSent db ft msgId
2022-05-05 10:37:53 +01:00
unless ( fileStatus == FSCancelled ) $ sendFileChunk user ft
2021-09-04 07:32:56 +01:00
MERR _ err -> do
2023-01-24 16:24:34 +04:00
cancelSndFileTransfer user ft True >>= mapM_ ( deleteAgentConnectionAsync user )
2021-09-04 07:32:56 +01:00
case err of
2022-05-05 10:37:53 +01:00
SMP SMP . AUTH -> unless ( fileStatus == FSCancelled ) $ do
2023-03-30 14:10:13 +04:00
ci <- withStore $ \ db -> do
getChatRefByFileId db user fileId >>= \ case
ChatRef CTDirect _ -> liftIO $ updateFileCancelled db user fileId CIFSSndCancelled
_ -> pure ()
getChatItemByFileId db user fileId
2023-01-04 21:06:28 +04:00
toView $ CRSndFileRcvCancelled user ci ft
2022-01-26 21:20:08 +00:00
_ -> throwChatError $ CEFileSend fileId err
2023-07-13 23:48:25 +01:00
MSG meta _ _ -> withAckMessage' agentConnId conn meta $ pure ()
2022-09-14 19:45:21 +04:00
OK ->
-- [async agent commands] continuation on receiving OK
withCompletedCommand conn agentMsg $ \ _cmdData -> pure ()
2022-09-16 19:30:02 +04:00
ERR err -> do
2023-01-09 17:02:38 +04:00
toView $ CRChatError ( Just user ) ( ChatErrorAgent err $ Just connEntity )
2022-09-16 19:30:02 +04:00
when ( corrId /= " " ) $ withCompletedCommand conn agentMsg $ \ _cmdData -> pure ()
2022-01-12 11:54:40 +00:00
-- TODO add debugging output
2021-09-04 07:32:56 +01:00
_ -> pure ()
2023-03-10 17:23:04 +00:00
processRcvFileConn :: ACommand 'Agent e -> ConnectionEntity -> Connection -> RcvFileTransfer -> m ()
2023-01-06 13:11:21 +04:00
processRcvFileConn agentMsg connEntity conn ft @ RcvFileTransfer { fileId , fileInvitation = FileInvitation { fileName } , grpMemberId } =
2021-09-04 07:32:56 +01:00
case agentMsg of
2022-12-12 16:33:07 +04:00
INV ( ACR _ cReq ) ->
withCompletedCommand conn agentMsg $ \ CommandData { cmdFunction } ->
case cReq of
fileInvConnReq @ ( CRInvitationUri _ _ ) -> case cmdFunction of
-- [async agent commands] direct XFileAcptInv continuation on receiving INV
CFCreateConnFileInvDirect -> do
ct <- withStore $ \ db -> getContactByFileId db user fileId
sharedMsgId <- withStore $ \ db -> getSharedMsgIdByFileId db userId fileId
void $ sendDirectContactMessage ct ( XFileAcptInv sharedMsgId ( Just fileInvConnReq ) fileName )
-- [async agent commands] group XFileAcptInv continuation on receiving INV
CFCreateConnFileInvGroup -> case grpMemberId of
Just gMemberId -> do
GroupMember { groupId , activeConn } <- withStore $ \ db -> getGroupMemberById db user gMemberId
case activeConn of
Just gMemberConn -> do
sharedMsgId <- withStore $ \ db -> getSharedMsgIdByFileId db userId fileId
void $ sendDirectMessage gMemberConn ( XFileAcptInv sharedMsgId ( Just fileInvConnReq ) fileName ) $ GroupId groupId
_ -> throwChatError $ CECommandError " no GroupMember activeConn "
_ -> throwChatError $ CECommandError " no grpMemberId "
_ -> throwChatError $ CECommandError " unexpected cmdFunction "
CRContactUri _ -> throwChatError $ CECommandError " unexpected ConnectionRequestUri type "
2022-05-11 16:18:28 +04:00
-- SMP CONF for RcvFileConnection happens for group file protocol
-- when sender of the file "joins" connection created by the recipient
-- (sender doesn't create connections for all group members)
2022-07-20 14:57:16 +01:00
CONF confId _ connInfo -> do
2023-09-01 19:20:07 +04:00
ChatMessage { chatVRange , chatMsgEvent } <- parseChatMessage conn connInfo
2023-09-06 11:41:23 +04:00
conn' <- updatePeerChatVRange conn chatVRange
2022-04-05 10:01:08 +04:00
case chatMsgEvent of
2023-09-01 19:20:07 +04:00
XOk -> allowAgentConnectionAsync user conn' confId XOk -- [async agent commands] no continuation needed, but command should be asynchronous for stability
2022-04-05 10:01:08 +04:00
_ -> pure ()
2023-03-16 10:49:57 +04:00
CON -> startReceivingFile user fileId
2022-10-14 13:06:33 +01:00
MSG meta _ msgBody -> do
2023-02-08 21:23:53 +04:00
parseFileChunk msgBody >>= receiveFileChunk ft ( Just conn ) meta
2022-09-14 19:45:21 +04:00
OK ->
-- [async agent commands] continuation on receiving OK
withCompletedCommand conn agentMsg $ \ _cmdData -> pure ()
2023-01-07 19:47:51 +04:00
MERR _ err -> do
2023-01-09 17:02:38 +04:00
toView $ CRChatError ( Just user ) ( ChatErrorAgent err $ Just connEntity )
2023-01-07 19:47:51 +04:00
incAuthErrCounter connEntity conn err
2022-09-16 19:30:02 +04:00
ERR err -> do
2023-01-09 17:02:38 +04:00
toView $ CRChatError ( Just user ) ( ChatErrorAgent err $ Just connEntity )
2022-09-16 19:30:02 +04:00
when ( corrId /= " " ) $ withCompletedCommand conn agentMsg $ \ _cmdData -> pure ()
2022-01-12 11:54:40 +00:00
-- TODO add debugging output
2021-09-04 07:32:56 +01:00
_ -> pure ()
2022-10-14 13:06:33 +01:00
receiveFileChunk :: RcvFileTransfer -> Maybe Connection -> MsgMeta -> FileChunk -> m ()
2023-04-18 12:48:36 +04:00
receiveFileChunk ft @ RcvFileTransfer { fileId , chunkSize } conn_ meta @ MsgMeta { recipient = ( msgId , _ ) , integrity } = \ case
2022-10-14 13:06:33 +01:00
FileChunkCancel ->
2023-04-18 12:48:36 +04:00
unless ( rcvFileCompleteOrCancelled ft ) $ do
2023-01-24 16:24:34 +04:00
cancelRcvFileTransfer user ft >>= mapM_ ( deleteAgentConnectionAsync user )
2023-03-29 17:18:44 +04:00
ci <- withStore $ \ db -> getChatItemByFileId db user fileId
toView $ CRRcvFileSndCancelled user ci ft
2022-10-14 13:06:33 +01:00
FileChunk { chunkNo , chunkBytes = chunk } -> do
case integrity of
MsgOk -> pure ()
MsgError MsgDuplicate -> pure () -- TODO remove once agent removes duplicates
MsgError e ->
badRcvFileChunk ft $ " invalid file chunk number " <> show chunkNo <> " : " <> show e
withStore' ( \ db -> createRcvFileChunk db ft chunkNo msgId ) >>= \ case
RcvChunkOk ->
if B . length chunk /= fromInteger chunkSize
then badRcvFileChunk ft " incorrect chunk size "
2023-02-08 21:23:53 +04:00
else ack $ appendFileChunk ft chunkNo chunk
2022-10-14 13:06:33 +01:00
RcvChunkFinal ->
if B . length chunk > fromInteger chunkSize
then badRcvFileChunk ft " incorrect chunk size "
else do
appendFileChunk ft chunkNo chunk
ci <- withStore $ \ db -> do
liftIO $ do
2023-03-16 10:49:57 +04:00
updateRcvFileStatus db fileId FSComplete
2022-10-14 13:06:33 +01:00
updateCIFileStatus db user fileId CIFSRcvComplete
deleteRcvFileChunks db ft
getChatItemByFileId db user fileId
2023-01-04 21:06:28 +04:00
toView $ CRRcvFileComplete user ci
2022-10-14 13:06:33 +01:00
closeFileHandle fileId rcvFiles
2023-01-24 16:24:34 +04:00
forM_ conn_ $ \ conn -> deleteAgentConnectionAsync user ( aConnId conn )
2023-02-08 21:23:53 +04:00
RcvChunkDuplicate -> ack $ pure ()
2022-10-14 13:06:33 +01:00
RcvChunkError -> badRcvFileChunk ft $ " incorrect chunk number " <> show chunkNo
2023-02-08 21:23:53 +04:00
where
ack a = case conn_ of
2023-07-13 23:48:25 +01:00
Just conn -> withAckMessage' agentConnId conn meta a
2023-02-08 21:23:53 +04:00
Nothing -> a
2022-10-14 13:06:33 +01:00
2023-03-10 17:23:04 +00:00
processUserContactRequest :: ACommand 'Agent e -> ConnectionEntity -> Connection -> UserContact -> m ()
2023-01-06 13:11:21 +04:00
processUserContactRequest agentMsg connEntity conn UserContact { userContactLinkId } = case agentMsg of
2022-07-20 14:57:16 +01:00
REQ invId _ connInfo -> do
2023-09-01 19:20:07 +04:00
ChatMessage { chatVRange , chatMsgEvent } <- parseChatMessage conn connInfo
2021-12-08 13:09:51 +00:00
case chatMsgEvent of
2023-09-01 19:20:07 +04:00
XContact p xContactId_ -> profileContactRequest invId chatVRange p xContactId_
XInfo p -> profileContactRequest invId chatVRange p Nothing
2021-12-08 13:09:51 +00:00
-- TODO show/log error, other events in contact request
_ -> pure ()
2023-01-07 19:47:51 +04:00
MERR _ err -> do
2023-01-09 17:02:38 +04:00
toView $ CRChatError ( Just user ) ( ChatErrorAgent err $ Just connEntity )
2023-01-07 19:47:51 +04:00
incAuthErrCounter connEntity conn err
2022-09-16 19:30:02 +04:00
ERR err -> do
2023-01-09 17:02:38 +04:00
toView $ CRChatError ( Just user ) ( ChatErrorAgent err $ Just connEntity )
2022-09-16 19:30:02 +04:00
when ( corrId /= " " ) $ withCompletedCommand conn agentMsg $ \ _cmdData -> pure ()
2022-01-12 11:54:40 +00:00
-- TODO add debugging output
2021-12-08 13:09:51 +00:00
_ -> pure ()
where
2023-09-01 19:20:07 +04:00
profileContactRequest :: InvitationId -> VersionRange -> Profile -> Maybe XContactId -> m ()
profileContactRequest invId chatVRange p xContactId_ = do
withStore ( \ db -> createOrUpdateContactRequest db user userContactLinkId invId chatVRange p xContactId_ ) >>= \ case
2023-01-04 21:06:28 +04:00
CORContact contact -> toView $ CRContactRequestAlreadyAccepted user contact
2022-05-14 00:57:24 +04:00
CORRequest cReq @ UserContactRequest { localDisplayName } -> do
2022-10-13 17:12:22 +04:00
withStore' ( \ db -> getUserContactLinkById db userId userContactLinkId ) >>= \ case
2023-03-06 09:51:42 +00:00
Just ( UserContactLink { autoAccept } , groupId_ , _ ) ->
2022-10-21 19:14:12 +03:00
case autoAccept of
Just AutoAccept { acceptIncognito } -> case groupId_ of
2022-10-13 17:12:22 +04:00
Nothing -> do
-- [incognito] generate profile to send, create connection with incognito profile
2022-10-21 19:14:12 +03:00
incognitoProfile <- if acceptIncognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing
2022-10-14 14:57:01 +04:00
ct <- acceptContactRequestAsync user cReq incognitoProfile
2023-01-04 21:06:28 +04:00
toView $ CRAcceptingContactRequest user ct
2022-10-13 17:12:22 +04:00
Just groupId -> do
2023-09-19 18:50:10 +01:00
gInfo <- withStore $ \ db -> getGroupInfo db user groupId
let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo
2022-10-14 14:57:01 +04:00
ct <- acceptContactRequestAsync user cReq profileMode
2023-01-04 21:06:28 +04:00
toView $ CRAcceptingGroupJoinRequest user gInfo ct
2022-10-21 19:14:12 +03:00
_ -> do
2023-01-04 21:06:28 +04:00
toView $ CRReceivedContactRequest user cReq
2023-03-22 15:58:01 +00:00
whenUserNtfs user $
showToast ( localDisplayName <> " > " ) " wants to connect to you "
2022-10-13 17:12:22 +04:00
_ -> pure ()
2021-12-08 13:09:51 +00:00
2023-01-07 19:47:51 +04:00
incAuthErrCounter :: ConnectionEntity -> Connection -> AgentErrorType -> m ()
incAuthErrCounter connEntity conn err = do
case err of
SMP SMP . AUTH -> do
authErrCounter' <- withStore' $ \ db -> incConnectionAuthErrCounter db user conn
when ( authErrCounter' >= authErrDisableCount ) $ do
toView $ CRConnectionDisabled connEntity
_ -> pure ()
2023-03-10 17:23:04 +00:00
updateChatLock :: MsgEncodingI enc => String -> ChatMsgEvent enc -> m ()
2022-10-22 21:22:44 +01:00
updateChatLock name event = do
l <- asks chatLock
atomically $ tryReadTMVar l >>= mapM_ ( swapTMVar l . ( <> s ) )
where
s = " " <> name <> " = " <> B . unpack ( strEncode $ toCMEventTag event )
2023-03-13 10:30:32 +00:00
withCompletedCommand :: forall e . AEntityI e => Connection -> ACommand 'Agent e -> ( CommandData -> m () ) -> m ()
2022-09-14 19:45:21 +04:00
withCompletedCommand Connection { connId } agentMsg action = do
2023-03-10 17:23:04 +00:00
let agentMsgTag = APCT ( sAEntity @ e ) $ aCommandTag agentMsg
2022-09-14 19:45:21 +04:00
cmdData_ <- withStore' $ \ db -> getCommandDataByCorrId db user corrId
case cmdData_ of
Just cmdData @ CommandData { cmdId , cmdConnId = Just cmdConnId' , cmdFunction }
2023-03-10 17:23:04 +00:00
| connId == cmdConnId' && ( agentMsgTag == commandExpectedResponse cmdFunction || agentMsgTag == APCT SAEConn ERR_ ) -> do
2022-10-15 14:48:07 +04:00
withStore' $ \ db -> deleteCommand db user cmdId
action cmdData
2022-09-16 19:30:02 +04:00
| otherwise -> err cmdId $ " not matching connection id or unexpected response, corrId = " <> show corrId
Just CommandData { cmdId , cmdConnId = Nothing } -> err cmdId $ " no command connection id, corrId = " <> show corrId
Nothing -> throwChatError . CEAgentCommandError $ " command not found, corrId = " <> show corrId
where
err cmdId msg = do
withStore' $ \ db -> updateCommandStatus db user cmdId CSError
throwChatError . CEAgentCommandError $ msg
2022-09-14 19:45:21 +04:00
createAckCmd :: Connection -> m CommandId
createAckCmd Connection { connId } = do
withStore' $ \ db -> createCommand db user ( Just connId ) CFAckMessage
2023-07-13 23:48:25 +01:00
withAckMessage' :: ConnId -> Connection -> MsgMeta -> m () -> m ()
withAckMessage' cId conn msgMeta action = do
cmdId <- createAckCmd conn
withAckMessage cId cmdId msgMeta $ action $> False
withAckMessage :: ConnId -> CommandId -> MsgMeta -> m Bool -> m ()
2023-07-09 23:24:38 +01:00
withAckMessage cId cmdId MsgMeta { recipient = ( msgId , _ ) } action = do
2022-09-14 19:45:21 +04:00
-- [async agent commands] command should be asynchronous, continuation is ackMsgDeliveryEvent
2023-07-13 23:48:25 +01:00
-- TODO catching error and sending ACK after an error, particularly if it is a database error, will result in the message not processed (and no notification to the user).
-- Possible solutions are:
-- 1) retry processing several times
-- 2) stabilize database
-- 3) show screen of death to the user asking to restart
tryChatError action >>= \ case
Right withRcpt -> ack $ if withRcpt then Just " " else Nothing
Left e -> ack Nothing >> throwError e
where
ack rcpt = withAgent $ \ a -> ackMessageAsync a ( aCorrId cmdId ) cId msgId rcpt
2022-09-14 19:45:21 +04:00
ackMsgDeliveryEvent :: Connection -> CommandId -> m ()
ackMsgDeliveryEvent Connection { connId } ackCmdId =
2023-01-06 14:22:16 +04:00
withStoreCtx'
( Just $ " createRcvMsgDeliveryEvent, connId: " <> show connId <> " , ackCmdId: " <> show ackCmdId <> " , msgDeliveryStatus: MDSRcvAcknowledged " )
$ \ db -> createRcvMsgDeliveryEvent db connId ackCmdId MDSRcvAcknowledged
2021-12-29 23:11:55 +04:00
sentMsgDeliveryEvent :: Connection -> AgentMsgId -> m ()
sentMsgDeliveryEvent Connection { connId } msgId =
2023-01-06 14:22:16 +04:00
withStoreCtx
( Just $ " createSndMsgDeliveryEvent, connId: " <> show connId <> " , msgId: " <> show msgId <> " , msgDeliveryStatus: MDSSndSent " )
$ \ db -> createSndMsgDeliveryEvent db connId msgId MDSSndSent
2021-12-29 23:11:55 +04:00
2022-02-07 15:19:34 +04:00
agentErrToItemStatus :: AgentErrorType -> CIStatus 'MDSnd
agentErrToItemStatus ( SMP AUTH ) = CISSndErrorAuth
2022-12-03 13:28:51 +00:00
agentErrToItemStatus err = CISSndError . T . unpack . safeDecodeUtf8 $ strEncode err
2022-02-07 15:19:34 +04:00
2021-09-04 07:32:56 +01:00
badRcvFileChunk :: RcvFileTransfer -> String -> m ()
2023-04-18 12:48:36 +04:00
badRcvFileChunk ft err =
unless ( rcvFileCompleteOrCancelled ft ) $ do
2023-01-24 16:24:34 +04:00
cancelRcvFileTransfer user ft >>= mapM_ ( deleteAgentConnectionAsync user )
2022-05-11 16:18:28 +04:00
throwChatError $ CEFileRcvChunk err
2021-09-04 07:32:56 +01:00
2022-08-27 19:56:03 +04:00
memberConnectedChatItem :: GroupInfo -> GroupMember -> m ()
2022-11-01 13:26:08 +00:00
memberConnectedChatItem gInfo m =
-- ts should be broker ts but we don't have it for CON
2022-11-23 11:04:08 +00:00
createInternalChatItem user ( CDGroupRcv gInfo m ) ( CIRcvGroupEvent RGEMemberConnected ) Nothing
2022-07-20 16:56:55 +04:00
2022-12-10 08:27:32 +00:00
groupDescriptionChatItem :: GroupInfo -> GroupMember -> Text -> m ()
groupDescriptionChatItem gInfo m descr =
createInternalChatItem user ( CDGroupRcv gInfo m ) ( CIRcvMsgContent $ MCText descr ) Nothing
2023-06-09 16:43:53 +04:00
notifyMemberConnected :: GroupInfo -> GroupMember -> Maybe Contact -> m ()
notifyMemberConnected gInfo m @ GroupMember { localDisplayName = c } ct_ = do
2022-08-27 19:56:03 +04:00
memberConnectedChatItem gInfo m
2023-06-09 16:43:53 +04:00
toView $ CRConnectedToGroupMember user gInfo m ct_
2022-01-27 22:01:15 +00:00
let g = groupName' gInfo
2023-03-22 15:58:01 +00:00
whenGroupNtfs user gInfo $ do
setActive $ ActiveG g
showToast ( " # " <> g ) $ " member " <> c <> " is connected "
2021-07-24 10:26:28 +01:00
2023-08-08 17:25:28 +04:00
probeMatchingContacts :: Contact -> IncognitoEnabled -> m ()
2022-08-27 19:56:03 +04:00
probeMatchingContacts ct connectedIncognito = do
2021-07-27 08:08:05 +01:00
gVar <- asks idsDrg
2022-08-27 19:56:03 +04:00
if connectedIncognito
2023-09-20 00:26:03 +04:00
then sendProbe . Probe =<< liftIO ( encodedRandomBytes gVar 32 )
2022-08-27 19:56:03 +04:00
else do
2023-09-20 00:26:03 +04:00
( probe , probeId ) <- withStore $ \ db -> createSentProbe db gVar userId ( CGMContact ct )
sendProbe probe
2022-11-15 10:31:44 +04:00
cs <- withStore' $ \ db -> getMatchingContacts db user ct
2023-09-20 00:26:03 +04:00
sendProbeHashes cs probe probeId
2021-07-27 08:08:05 +01:00
where
2023-09-20 00:26:03 +04:00
sendProbe :: Probe -> m ()
sendProbe probe = void . sendDirectContactMessage ct $ XInfoProbe probe
probeMatchingMemberContact :: GroupInfo -> GroupMember -> IncognitoEnabled -> m ()
probeMatchingMemberContact _ GroupMember { activeConn = Nothing } _ = pure ()
probeMatchingMemberContact g m @ GroupMember { groupId , activeConn = Just conn } connectedIncognito = do
gVar <- asks idsDrg
if connectedIncognito
then sendProbe . Probe =<< liftIO ( encodedRandomBytes gVar 32 )
else do
( probe , probeId ) <- withStore $ \ db -> createSentProbe db gVar userId $ CGMGroupMember g m
sendProbe probe
cs <- withStore' $ \ db -> getMatchingMemberContacts db user m
sendProbeHashes cs probe probeId
where
sendProbe :: Probe -> m ()
sendProbe probe = void $ sendDirectMessage conn ( XInfoProbe probe ) ( GroupId groupId )
-- TODO currently we only send probe hashes to contacts
sendProbeHashes :: [ Contact ] -> Probe -> Int64 -> m ()
sendProbeHashes cs probe probeId =
forM_ cs $ \ c -> sendProbeHash c ` catchChatError ` \ _ -> pure ()
where
probeHash = ProbeHash $ C . sha256Hash ( unProbe probe )
sendProbeHash :: Contact -> m ()
sendProbeHash c = do
2022-02-14 18:49:42 +04:00
void . sendDirectContactMessage c $ XInfoProbeCheck probeHash
2023-09-20 00:26:03 +04:00
withStore' $ \ db -> createSentProbeHash db userId probeId $ CGMContact c
2021-07-27 08:08:05 +01:00
2021-07-24 10:26:28 +01:00
messageWarning :: Text -> m ()
2023-01-04 21:06:28 +04:00
messageWarning = toView . CRMessageError user " warning "
2021-07-24 10:26:28 +01:00
messageError :: Text -> m ()
2023-01-04 21:06:28 +04:00
messageError = toView . CRMessageError user " error "
2021-07-24 10:26:28 +01:00
2022-03-16 13:20:47 +00:00
newContentMessage :: Contact -> MsgContainer -> RcvMessage -> MsgMeta -> m ()
2023-03-22 15:58:01 +00:00
newContentMessage ct @ Contact { localDisplayName = c , contactUsed } mc msg @ RcvMessage { sharedMsgId_ } msgMeta = do
2022-10-25 12:50:26 +04:00
unless contactUsed $ withStore' $ \ db -> updateContactUsed db user ct
2022-05-28 19:13:07 +01:00
checkIntegrityCreateItem ( CDDirectRcv ct ) msgMeta
2023-03-13 10:30:32 +00:00
let ExtMsgContent content fInv_ _ _ = mcExtMsgContent mc
2023-07-09 23:24:38 +01:00
-- Uncomment to test stuck delivery on errors - see test testDirectMessageDelete
-- case content of
-- MCText "hello 111" ->
-- UE.throwIO $ userError "#####################"
-- -- throwChatError $ CECommandError "#####################"
-- _ -> pure ()
2022-12-13 14:52:34 +00:00
if isVoice content && not ( featureAllowed SCFVoice forContact ct )
2022-12-07 09:58:01 +00:00
then do
2022-12-16 07:51:04 +00:00
void $ newChatItem ( CIRcvChatFeatureRejected CFVoice ) Nothing Nothing False
2022-12-07 09:58:01 +00:00
setActive $ ActiveC c
2022-11-30 19:42:33 +04:00
else do
2022-12-16 07:51:04 +00:00
let ExtMsgContent _ _ itemTTL live_ = mcExtMsgContent mc
2022-12-20 10:17:29 +00:00
timed_ = rcvContactCITimed ct itemTTL
2022-12-16 07:51:04 +00:00
live = fromMaybe False live_
2023-06-16 13:43:06 +01:00
file_ <- processFileInvitation fInv_ content $ \ db -> createRcvFileTransfer db userId ct
ChatItem { formattedText } <- newChatItem ( CIRcvMsgContent content ) ( snd <$> file_ ) timed_ live
autoAcceptFile file_
2023-03-22 15:58:01 +00:00
whenContactNtfs user ct $ do
2022-12-07 09:58:01 +00:00
showMsgToast ( c <> " > " ) content formattedText
2022-12-07 22:18:22 +00:00
setActive $ ActiveC c
2022-11-23 11:04:08 +00:00
where
2022-12-16 07:51:04 +00:00
newChatItem ciContent ciFile_ timed_ live = do
ci <- saveRcvChatItem' user ( CDDirectRcv ct ) msg sharedMsgId_ msgMeta ciContent ciFile_ timed_ live
2023-05-15 12:28:53 +02:00
reactions <- maybe ( pure [] ) ( \ sharedMsgId -> withStore' $ \ db -> getDirectCIReactions db ct sharedMsgId ) sharedMsgId_
toView $ CRNewChatItem user ( AChatItem SCTDirect SMDRcv ( DirectChat ct ) ci { reactions } )
2022-11-23 11:04:08 +00:00
pure ci
2022-01-11 08:50:44 +00:00
2023-06-16 13:43:06 +01:00
autoAcceptFile :: Maybe ( RcvFileTransfer , CIFile 'MDRcv ) -> m ()
autoAcceptFile = mapM_ $ \ ( ft , CIFile { fileSize } ) -> do
ChatConfig { autoAcceptFileSize = sz } <- asks config
when ( sz > fileSize ) $ receiveFile' user ft Nothing Nothing >>= toView
2023-03-13 10:30:32 +00:00
messageFileDescription :: Contact -> SharedMsgId -> FileDescr -> MsgMeta -> m ()
2023-03-14 11:42:44 +04:00
messageFileDescription ct @ Contact { contactId } sharedMsgId fileDescr msgMeta = do
2023-03-13 10:30:32 +00:00
checkIntegrityCreateItem ( CDDirectRcv ct ) msgMeta
2023-03-14 11:42:44 +04:00
fileId <- withStore $ \ db -> getFileIdBySharedMsgId db userId contactId sharedMsgId
processFDMessage fileId fileDescr
2023-03-13 10:30:32 +00:00
groupMessageFileDescription :: GroupInfo -> GroupMember -> SharedMsgId -> FileDescr -> MsgMeta -> m ()
2023-03-14 11:42:44 +04:00
groupMessageFileDescription GroupInfo { groupId } _m sharedMsgId fileDescr _msgMeta = do
fileId <- withStore $ \ db -> getGroupFileIdBySharedMsgId db userId groupId sharedMsgId
processFDMessage fileId fileDescr
processFDMessage :: FileTransferId -> FileDescr -> m ()
processFDMessage fileId fileDescr = do
2023-04-18 12:48:36 +04:00
ft <- withStore $ \ db -> getRcvFileTransfer db user fileId
unless ( rcvFileCompleteOrCancelled ft ) $ do
2023-09-01 19:43:27 +01:00
( rfd , RcvFileTransfer { fileStatus , xftpRcvFile } ) <- withStore $ \ db -> do
2023-04-03 18:49:22 +04:00
rfd <- appendRcvFD db userId fileId fileDescr
-- reading second time in the same transaction as appending description
-- to prevent race condition with accept
2023-04-18 12:48:36 +04:00
ft' <- getRcvFileTransfer db user fileId
pure ( rfd , ft' )
2023-09-01 19:43:27 +01:00
case ( fileStatus , xftpRcvFile ) of
( RFSAccepted _ , Just XFTPRcvFile { cryptoArgs } ) -> receiveViaCompleteFD user fileId rfd cryptoArgs
2023-04-03 18:49:22 +04:00
_ -> pure ()
2023-03-13 10:30:32 +00:00
cancelMessageFile :: Contact -> SharedMsgId -> MsgMeta -> m ()
cancelMessageFile ct _sharedMsgId msgMeta = do
checkIntegrityCreateItem ( CDDirectRcv ct ) msgMeta
-- find the original chat item and file
2023-03-14 11:42:44 +04:00
-- mark file as cancelled, remove description if exists
2023-03-13 10:30:32 +00:00
pure ()
cancelGroupMessageFile :: GroupInfo -> GroupMember -> SharedMsgId -> MsgMeta -> m ()
cancelGroupMessageFile _gInfo _m _sharedMsgId _msgMeta = do
pure ()
2023-06-16 13:43:06 +01:00
processFileInvitation :: Maybe FileInvitation -> MsgContent -> ( DB . Connection -> FileInvitation -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer ) -> m ( Maybe ( RcvFileTransfer , CIFile 'MDRcv ) )
2022-11-26 22:39:56 +00:00
processFileInvitation fInv_ mc createRcvFT = forM fInv_ $ \ fInv @ FileInvitation { fileName , fileSize } -> do
2023-03-13 10:30:32 +00:00
ChatConfig { fileChunkSize } <- asks config
inline <- receiveInlineMode fInv ( Just mc ) fileChunkSize
2023-03-28 22:20:06 +04:00
ft @ RcvFileTransfer { fileId , xftpRcvFile } <- withStore $ \ db -> createRcvFT db fInv inline fileChunkSize
let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP
2022-10-14 13:06:33 +01:00
( filePath , fileStatus ) <- case inline of
Just IFMSent -> do
2023-03-16 10:49:57 +04:00
fPath <- getRcvFilePath fileId Nothing fileName True
2023-01-23 19:55:19 +04:00
withStore' $ \ db -> startRcvInlineFT db user ft fPath inline
2022-10-14 13:06:33 +01:00
pure ( Just fPath , CIFSRcvAccepted )
_ -> pure ( Nothing , CIFSRcvInvitation )
2023-09-01 19:43:27 +01:00
let fileSource = CF . plain <$> filePath
pure ( ft , CIFile { fileId , fileName , fileSize , fileSource , fileStatus , fileProtocol } )
2022-04-10 13:30:58 +04:00
2022-12-16 07:51:04 +00:00
messageUpdate :: Contact -> SharedMsgId -> MsgContent -> RcvMessage -> MsgMeta -> Maybe Int -> Maybe Bool -> m ()
messageUpdate ct @ Contact { contactId , localDisplayName = c } sharedMsgId mc msg @ RcvMessage { msgId } msgMeta ttl live_ = do
2022-05-28 19:13:07 +01:00
checkIntegrityCreateItem ( CDDirectRcv ct ) msgMeta
2023-05-15 12:28:53 +02:00
updateRcvChatItem ` catchCINotFound ` \ _ -> do
-- This patches initial sharedMsgId into chat item when locally deleted chat item
-- received an update from the sender, so that it can be referenced later (e.g. by broadcast delete).
-- Chat item and update message which created it will have different sharedMsgId in this case...
let timed_ = rcvContactCITimed ct ttl
ci <- saveRcvChatItem' user ( CDDirectRcv ct ) msg ( Just sharedMsgId ) msgMeta content Nothing timed_ live
ci' <- withStore' $ \ db -> do
createChatItemVersion db ( chatItemId' ci ) brokerTs mc
updateDirectChatItem' db user contactId ci content live Nothing
toView $ CRChatItemUpdated user ( AChatItem SCTDirect SMDRcv ( DirectChat ct ) ci' )
setActive $ ActiveC c
2022-05-17 11:22:09 +04:00
where
2023-05-08 20:07:51 +04:00
MsgMeta { broker = ( _ , brokerTs ) } = msgMeta
2022-12-19 11:16:50 +00:00
content = CIRcvMsgContent mc
2022-12-16 07:51:04 +00:00
live = fromMaybe False live_
2022-05-17 11:22:09 +04:00
updateRcvChatItem = do
2023-05-08 20:07:51 +04:00
cci <- withStore $ \ db -> getDirectChatItemBySharedMsgId db user contactId sharedMsgId
case cci of
2023-05-11 16:00:01 +04:00
CChatItem SMDRcv ci @ ChatItem { meta = CIMeta { itemLive } , content = CIRcvMsgContent oldMC } -> do
let changed = mc /= oldMC
if changed || fromMaybe False itemLive
2023-05-09 20:43:21 +04:00
then do
ci' <- withStore' $ \ db -> do
2023-05-11 16:00:01 +04:00
when changed $
addInitialAndNewCIVersions db ( chatItemId' ci ) ( chatItemTs' ci , oldMC ) ( brokerTs , mc )
2023-05-09 20:43:21 +04:00
updateDirectChatItem' db user contactId ci content live $ Just msgId
toView $ CRChatItemUpdated user ( AChatItem SCTDirect SMDRcv ( DirectChat ct ) ci' )
startUpdatedTimedItemThread user ( ChatRef CTDirect contactId ) ci ci'
2023-05-11 16:00:01 +04:00
else toView $ CRChatItemNotChanged user ( AChatItem SCTDirect SMDRcv ( DirectChat ct ) ci )
2023-05-08 20:07:51 +04:00
_ -> messageError " x.msg.update: contact attempted invalid message update "
2022-03-28 20:35:57 +04:00
messageDelete :: Contact -> SharedMsgId -> RcvMessage -> MsgMeta -> m ()
2023-05-19 14:52:51 +02:00
messageDelete ct @ Contact { contactId } sharedMsgId RcvMessage { msgId } msgMeta @ MsgMeta { broker = ( _ , brokerTs ) } = do
2022-05-28 19:13:07 +01:00
checkIntegrityCreateItem ( CDDirectRcv ct ) msgMeta
2023-05-15 12:28:53 +02:00
deleteRcvChatItem ` catchCINotFound ` ( toView . CRChatItemDeletedNotFound user ct )
2022-05-17 11:22:09 +04:00
where
deleteRcvChatItem = do
2022-12-20 12:58:15 +00:00
ci @ ( CChatItem msgDir _ ) <- withStore $ \ db -> getDirectChatItemBySharedMsgId db user contactId sharedMsgId
2022-05-17 11:22:09 +04:00
case msgDir of
2022-11-30 19:42:33 +04:00
SMDRcv ->
2022-12-13 14:52:34 +00:00
if featureAllowed SCFFullDelete forContact ct
2022-12-15 15:17:29 +04:00
then deleteDirectCI user ct ci False False >>= toView
2023-05-19 14:52:51 +02:00
else markDirectCIDeleted user ct ci msgId False brokerTs >>= toView
2022-05-17 11:22:09 +04:00
SMDSnd -> messageError " x.msg.del: contact attempted invalid message delete "
2022-03-23 11:37:51 +00:00
2023-05-15 12:28:53 +02:00
directMsgReaction :: Contact -> SharedMsgId -> MsgReaction -> Bool -> RcvMessage -> MsgMeta -> m ()
directMsgReaction ct sharedMsgId reaction add RcvMessage { msgId } MsgMeta { broker = ( _ , brokerTs ) } = do
when ( featureAllowed SCFReactions forContact ct ) $ do
rs <- withStore' $ \ db -> getDirectReactions db ct sharedMsgId False
when ( reactionAllowed add reaction rs ) $ do
updateChatItemReaction ` catchCINotFound ` \ _ ->
withStore' $ \ db -> setDirectReaction db ct sharedMsgId False reaction add msgId brokerTs
where
updateChatItemReaction = do
cr_ <- withStore $ \ db -> do
CChatItem md ci <- getDirectChatItemBySharedMsgId db user ( contactId' ct ) sharedMsgId
if ciReactionAllowed ci
then liftIO $ do
setDirectReaction db ct sharedMsgId False reaction add msgId brokerTs
reactions <- getDirectCIReactions db ct sharedMsgId
let ci' = CChatItem md ci { reactions }
r = ACIReaction SCTDirect SMDRcv ( DirectChat ct ) $ CIReaction CIDirectRcv ci' brokerTs reaction
2023-05-17 01:22:00 +02:00
pure $ Just $ CRChatItemReaction user add r
2023-05-15 12:28:53 +02:00
else pure Nothing
mapM_ toView cr_
groupMsgReaction :: GroupInfo -> GroupMember -> SharedMsgId -> MemberId -> MsgReaction -> Bool -> RcvMessage -> MsgMeta -> m ()
groupMsgReaction g @ GroupInfo { groupId } m sharedMsgId itemMemberId reaction add RcvMessage { msgId } MsgMeta { broker = ( _ , brokerTs ) } = do
when ( groupFeatureAllowed SGFReactions g ) $ do
rs <- withStore' $ \ db -> getGroupReactions db g m itemMemberId sharedMsgId False
when ( reactionAllowed add reaction rs ) $ do
updateChatItemReaction ` catchCINotFound ` \ _ ->
withStore' $ \ db -> setGroupReaction db g m itemMemberId sharedMsgId False reaction add msgId brokerTs
where
updateChatItemReaction = do
cr_ <- withStore $ \ db -> do
CChatItem md ci <- getGroupMemberCIBySharedMsgId db user groupId itemMemberId sharedMsgId
if ciReactionAllowed ci
then liftIO $ do
setGroupReaction db g m itemMemberId sharedMsgId False reaction add msgId brokerTs
reactions <- getGroupCIReactions db g itemMemberId sharedMsgId
let ci' = CChatItem md ci { reactions }
2023-05-15 21:07:03 +04:00
r = ACIReaction SCTGroup SMDRcv ( GroupChat g ) $ CIReaction ( CIGroupRcv m ) ci' brokerTs reaction
2023-05-17 01:22:00 +02:00
pure $ Just $ CRChatItemReaction user add r
2023-05-15 12:28:53 +02:00
else pure Nothing
mapM_ toView cr_
reactionAllowed :: Bool -> MsgReaction -> [ MsgReaction ] -> Bool
reactionAllowed add reaction rs = ( reaction ` elem ` rs ) /= add && not ( add && length rs >= maxMsgReactions )
catchCINotFound :: m a -> ( SharedMsgId -> m a ) -> m a
catchCINotFound f handle =
2023-07-09 23:24:38 +01:00
f ` catchChatError ` \ case
2023-05-15 12:28:53 +02:00
ChatErrorStore ( SEChatItemSharedMsgIdNotFound sharedMsgId ) -> handle sharedMsgId
e -> throwError e
2022-03-16 13:20:47 +00:00
newGroupContentMessage :: GroupInfo -> GroupMember -> MsgContainer -> RcvMessage -> MsgMeta -> m ()
2023-06-24 12:36:07 +01:00
newGroupContentMessage gInfo m @ GroupMember { localDisplayName = c , memberId , memberRole } mc msg @ RcvMessage { sharedMsgId_ } msgMeta
| isVoice content && not ( groupFeatureAllowed SGFVoice gInfo ) = rejected GFVoice
| not ( isVoice content ) && isJust fInv_ && not ( groupFeatureAllowed SGFFiles gInfo ) = rejected GFFiles
| otherwise = do
-- TODO integrity message check
-- check if message moderation event was received ahead of message
let timed_ = rcvGroupCITimed gInfo itemTTL
live = fromMaybe False live_
withStore' ( \ db -> getCIModeration db user gInfo memberId sharedMsgId_ ) >>= \ case
Just ciModeration -> do
applyModeration timed_ live ciModeration
withStore' $ \ db -> deleteCIModeration db gInfo memberId sharedMsgId_
Nothing -> createItem timed_ live
2023-06-22 20:38:09 +04:00
where
2023-06-24 12:36:07 +01:00
rejected f = void $ newChatItem ( CIRcvGroupFeatureRejected f ) Nothing Nothing False
2023-06-22 20:38:09 +04:00
ExtMsgContent content fInv_ itemTTL live_ = mcExtMsgContent mc
applyModeration timed_ live CIModeration { moderatorMember = moderator @ GroupMember { memberRole = moderatorRole } , createdByMsgId , moderatedAt }
| moderatorRole < GRAdmin || moderatorRole < memberRole =
createItem timed_ live
| groupFeatureAllowed SGFFullDelete gInfo = do
ci <- saveRcvChatItem' user ( CDGroupRcv gInfo m ) msg sharedMsgId_ msgMeta CIRcvModerated Nothing timed_ False
ci' <- withStore' $ \ db -> updateGroupChatItemModerated db user gInfo ( CChatItem SMDRcv ci ) moderator moderatedAt
toView $ CRNewChatItem user ci'
| otherwise = do
file_ <- processFileInvitation fInv_ content $ \ db -> createRcvGroupFileTransfer db userId m
ci <- saveRcvChatItem' user ( CDGroupRcv gInfo m ) msg sharedMsgId_ msgMeta ( CIRcvMsgContent content ) ( snd <$> file_ ) timed_ False
cr <- markGroupCIDeleted user gInfo ( CChatItem SMDRcv ci ) createdByMsgId False ( Just moderator ) moderatedAt
toView cr
createItem timed_ live = do
2023-06-16 13:43:06 +01:00
file_ <- processFileInvitation fInv_ content $ \ db -> createRcvGroupFileTransfer db userId m
ChatItem { formattedText } <- newChatItem ( CIRcvMsgContent content ) ( snd <$> file_ ) timed_ live
autoAcceptFile file_
2022-11-23 11:04:08 +00:00
let g = groupName' gInfo
2023-03-22 15:58:01 +00:00
whenGroupNtfs user gInfo $ do
2022-12-07 09:58:01 +00:00
showMsgToast ( " # " <> g <> " " <> c <> " > " ) content formattedText
setActive $ ActiveG g
2022-12-16 07:51:04 +00:00
newChatItem ciContent ciFile_ timed_ live = do
ci <- saveRcvChatItem' user ( CDGroupRcv gInfo m ) msg sharedMsgId_ msgMeta ciContent ciFile_ timed_ live
2023-05-15 12:28:53 +02:00
reactions <- maybe ( pure [] ) ( \ sharedMsgId -> withStore' $ \ db -> getGroupCIReactions db gInfo memberId sharedMsgId ) sharedMsgId_
groupMsgToView gInfo m ci { reactions } msgMeta
2022-11-23 11:04:08 +00:00
pure ci
2021-07-07 22:46:38 +01:00
2022-12-16 07:51:04 +00:00
groupMessageUpdate :: GroupInfo -> GroupMember -> SharedMsgId -> MsgContent -> RcvMessage -> MsgMeta -> Maybe Int -> Maybe Bool -> m ()
groupMessageUpdate gInfo @ GroupInfo { groupId , localDisplayName = g } m @ GroupMember { groupMemberId , memberId } sharedMsgId mc msg @ RcvMessage { msgId } msgMeta ttl_ live_ =
2023-05-15 12:28:53 +02:00
updateRcvChatItem ` catchCINotFound ` \ _ -> do
-- This patches initial sharedMsgId into chat item when locally deleted chat item
-- received an update from the sender, so that it can be referenced later (e.g. by broadcast delete).
-- Chat item and update message which created it will have different sharedMsgId in this case...
let timed_ = rcvGroupCITimed gInfo ttl_
ci <- saveRcvChatItem' user ( CDGroupRcv gInfo m ) msg ( Just sharedMsgId ) msgMeta content Nothing timed_ live
ci' <- withStore' $ \ db -> do
createChatItemVersion db ( chatItemId' ci ) brokerTs mc
updateGroupChatItem db user groupId ci content live Nothing
toView $ CRChatItemUpdated user ( AChatItem SCTGroup SMDRcv ( GroupChat gInfo ) ci' )
setActive $ ActiveG g
2022-10-01 14:31:21 +04:00
where
2023-05-08 20:07:51 +04:00
MsgMeta { broker = ( _ , brokerTs ) } = msgMeta
2022-12-19 11:16:50 +00:00
content = CIRcvMsgContent mc
2022-12-16 07:51:04 +00:00
live = fromMaybe False live_
2022-10-01 14:31:21 +04:00
updateRcvChatItem = do
2023-05-08 20:07:51 +04:00
cci <- withStore $ \ db -> getGroupChatItemBySharedMsgId db user groupId groupMemberId sharedMsgId
case cci of
2023-05-11 16:00:01 +04:00
CChatItem SMDRcv ci @ ChatItem { chatDir = CIGroupRcv m' , meta = CIMeta { itemLive } , content = CIRcvMsgContent oldMC } ->
if sameMemberId memberId m'
2022-10-01 14:31:21 +04:00
then do
2023-05-11 16:00:01 +04:00
let changed = mc /= oldMC
if changed || fromMaybe False itemLive
then do
ci' <- withStore' $ \ db -> do
when changed $
addInitialAndNewCIVersions db ( chatItemId' ci ) ( chatItemTs' ci , oldMC ) ( brokerTs , mc )
updateGroupChatItem db user groupId ci content live $ Just msgId
toView $ CRChatItemUpdated user ( AChatItem SCTGroup SMDRcv ( GroupChat gInfo ) ci' )
setActive $ ActiveG g
startUpdatedTimedItemThread user ( ChatRef CTGroup groupId ) ci ci'
else toView $ CRChatItemNotChanged user ( AChatItem SCTGroup SMDRcv ( GroupChat gInfo ) ci )
else messageError " x.msg.update: group member attempted to update a message of another member "
2023-05-08 20:07:51 +04:00
_ -> messageError " x.msg.update: group member attempted invalid message update "
2022-03-28 20:35:57 +04:00
2023-05-19 14:52:51 +02:00
groupMessageDelete :: GroupInfo -> GroupMember -> SharedMsgId -> Maybe MemberId -> RcvMessage -> MsgMeta -> m ()
groupMessageDelete gInfo @ GroupInfo { groupId , membership } m @ GroupMember { memberId , memberRole = senderRole } sharedMsgId sndMemberId_ RcvMessage { msgId } MsgMeta { broker = ( _ , brokerTs ) } = do
2023-02-08 07:08:53 +00:00
let msgMemberId = fromMaybe memberId sndMemberId_
withStore' ( \ db -> runExceptT $ getGroupMemberCIBySharedMsgId db user groupId msgMemberId sharedMsgId ) >>= \ case
Right ci @ ( CChatItem _ ChatItem { chatDir } ) -> case chatDir of
CIGroupRcv mem
| sameMemberId memberId mem && msgMemberId == memberId -> delete ci Nothing >>= toView
| otherwise -> deleteMsg mem ci
CIGroupSnd -> deleteMsg membership ci
2023-06-22 20:38:09 +04:00
Left e
| msgMemberId == memberId -> messageError $ " x.msg.del: message not found, " <> tshow e
| senderRole < GRAdmin -> messageError $ " x.msg.del: message not found, message of another member with insufficient member permissions, " <> tshow e
| otherwise -> withStore' $ \ db -> createCIModeration db gInfo m msgMemberId sharedMsgId msgId brokerTs
2023-02-08 07:08:53 +00:00
where
deleteMsg :: GroupMember -> CChatItem 'CTGroup -> m ()
deleteMsg mem ci = case sndMemberId_ of
Just sndMemberId
| sameMemberId sndMemberId mem -> checkRole mem $ delete ci ( Just m ) >>= toView
| otherwise -> messageError " x.msg.del: message of another member with incorrect memberId "
_ -> messageError " x.msg.del: message of another member without memberId "
checkRole GroupMember { memberRole } a
| senderRole < GRAdmin || senderRole < memberRole =
messageError " x.msg.del: message of another member with insufficient member permissions "
| otherwise = a
delete ci byGroupMember
2023-05-19 14:52:51 +02:00
| groupFeatureAllowed SGFFullDelete gInfo = deleteGroupCI user gInfo ci False False byGroupMember brokerTs
| otherwise = markGroupCIDeleted user gInfo ci msgId False byGroupMember brokerTs
2022-03-23 11:37:51 +00:00
2022-04-10 13:30:58 +04:00
-- TODO remove once XFile is discontinued
processFileInvitation' :: Contact -> FileInvitation -> RcvMessage -> MsgMeta -> m ()
2022-12-16 07:51:04 +00:00
processFileInvitation' ct @ Contact { localDisplayName = c } fInv @ FileInvitation { fileName , fileSize } msg @ RcvMessage { sharedMsgId_ } msgMeta = do
2022-05-28 19:13:07 +01:00
checkIntegrityCreateItem ( CDDirectRcv ct ) msgMeta
2023-03-13 10:30:32 +00:00
ChatConfig { fileChunkSize } <- asks config
inline <- receiveInlineMode fInv Nothing fileChunkSize
2023-03-28 22:20:06 +04:00
RcvFileTransfer { fileId , xftpRcvFile } <- withStore $ \ db -> createRcvFileTransfer db userId ct fInv inline fileChunkSize
let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP
2023-09-01 19:43:27 +01:00
ciFile = Just $ CIFile { fileId , fileName , fileSize , fileSource = Nothing , fileStatus = CIFSRcvInvitation , fileProtocol }
2022-12-16 07:51:04 +00:00
ci <- saveRcvChatItem' user ( CDDirectRcv ct ) msg sharedMsgId_ msgMeta ( CIRcvMsgContent $ MCFile " " ) ciFile Nothing False
2023-01-04 21:06:28 +04:00
toView $ CRNewChatItem user ( AChatItem SCTDirect SMDRcv ( DirectChat ct ) ci )
2023-03-22 15:58:01 +00:00
whenContactNtfs user ct $ do
showToast ( c <> " > " ) " wants to send a file "
setActive $ ActiveC c
2021-09-04 07:32:56 +01:00
2022-04-10 13:30:58 +04:00
-- TODO remove once XFile is discontinued
processGroupFileInvitation' :: GroupInfo -> GroupMember -> FileInvitation -> RcvMessage -> MsgMeta -> m ()
2022-12-16 07:51:04 +00:00
processGroupFileInvitation' gInfo m @ GroupMember { localDisplayName = c } fInv @ FileInvitation { fileName , fileSize } msg @ RcvMessage { sharedMsgId_ } msgMeta = do
2023-03-13 10:30:32 +00:00
ChatConfig { fileChunkSize } <- asks config
inline <- receiveInlineMode fInv Nothing fileChunkSize
2023-03-28 22:20:06 +04:00
RcvFileTransfer { fileId , xftpRcvFile } <- withStore $ \ db -> createRcvGroupFileTransfer db userId m fInv inline fileChunkSize
let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP
2023-09-01 19:43:27 +01:00
ciFile = Just $ CIFile { fileId , fileName , fileSize , fileSource = Nothing , fileStatus = CIFSRcvInvitation , fileProtocol }
2022-12-16 07:51:04 +00:00
ci <- saveRcvChatItem' user ( CDGroupRcv gInfo m ) msg sharedMsgId_ msgMeta ( CIRcvMsgContent $ MCFile " " ) ciFile Nothing False
2022-05-28 19:13:07 +01:00
groupMsgToView gInfo m ci msgMeta
2022-01-27 22:01:15 +00:00
let g = groupName' gInfo
2023-03-22 15:58:01 +00:00
whenGroupNtfs user gInfo $ do
showToast ( " # " <> g <> " " <> c <> " > " ) " wants to send a file "
setActive $ ActiveG g
2021-09-05 14:08:29 +01:00
2022-11-26 22:39:56 +00:00
receiveInlineMode :: FileInvitation -> Maybe MsgContent -> Integer -> m ( Maybe InlineFileMode )
2023-03-13 10:30:32 +00:00
receiveInlineMode FileInvitation { fileSize , fileInline , fileDescr } mc_ chSize = case ( fileInline , fileDescr ) of
( Just mode , Nothing ) -> do
2022-11-26 22:39:56 +00:00
InlineFilesConfig { receiveChunks , receiveInstant } <- asks $ inlineFiles . config
pure $ if fileSize <= receiveChunks * chSize then inline' receiveInstant else Nothing
where
inline' receiveInstant = if mode == IFMOffer || ( receiveInstant && maybe False isVoice mc_ ) then fileInline else Nothing
2022-10-14 13:06:33 +01:00
_ -> pure Nothing
2022-05-11 16:18:28 +04:00
xFileCancel :: Contact -> SharedMsgId -> MsgMeta -> m ()
2022-05-28 19:13:07 +01:00
xFileCancel ct @ Contact { contactId } sharedMsgId msgMeta = do
checkIntegrityCreateItem ( CDDirectRcv ct ) msgMeta
2022-06-18 20:06:13 +01:00
fileId <- withStore $ \ db -> getFileIdBySharedMsgId db userId contactId sharedMsgId
2023-04-18 12:48:36 +04:00
ft <- withStore ( \ db -> getRcvFileTransfer db user fileId )
unless ( rcvFileCompleteOrCancelled ft ) $ do
2023-01-24 16:24:34 +04:00
cancelRcvFileTransfer user ft >>= mapM_ ( deleteAgentConnectionAsync user )
2023-03-29 17:18:44 +04:00
ci <- withStore $ \ db -> getChatItemByFileId db user fileId
toView $ CRRcvFileSndCancelled user ci ft
2022-05-11 16:18:28 +04:00
2022-10-14 13:06:33 +01:00
xFileAcptInv :: Contact -> SharedMsgId -> Maybe ConnReqInvitation -> String -> MsgMeta -> m ()
xFileAcptInv ct sharedMsgId fileConnReq_ fName msgMeta = do
2022-09-20 14:46:30 +01:00
checkIntegrityCreateItem ( CDDirectRcv ct ) msgMeta
fileId <- withStore $ \ db -> getDirectFileIdBySharedMsgId db user ct sharedMsgId
2023-04-18 18:51:14 +04:00
( AChatItem _ _ _ ci ) <- withStore $ \ db -> getChatItemByFileId db user fileId
assertSMPAcceptNotProhibited ci
2022-10-14 13:06:33 +01:00
ft @ FileTransferMeta { fileName , fileSize , fileInline , cancelled } <- withStore ( \ db -> getFileTransferMeta db user fileId )
2022-09-20 14:46:30 +01:00
-- [async agent commands] no continuation needed, but command should be asynchronous for stability
if fName == fileName
2022-10-14 13:06:33 +01:00
then unless cancelled $ case fileConnReq_ of
-- receiving via a separate connection
Just fileConnReq -> do
2023-09-10 22:40:15 +03:00
subMode <- chatReadVar subscriptionMode
dm <- directMessage XOk
connIds <- joinAgentConnectionAsync user True fileConnReq dm subMode
withStore' $ \ db -> createSndDirectFTConnection db user fileId connIds subMode
2022-10-14 13:06:33 +01:00
-- receiving inline
_ -> do
event <- withStore $ \ db -> do
2023-04-18 18:51:14 +04:00
ci' <- updateDirectCIFileStatus db user fileId $ CIFSSndTransfer 0 1
2022-10-14 13:06:33 +01:00
sft <- liftIO $ createSndDirectInlineFT db ct ft
2023-04-18 18:51:14 +04:00
pure $ CRSndFileStart user ci' sft
2022-10-14 13:06:33 +01:00
toView event
ifM
( allowSendInline fileSize fileInline )
( sendDirectFileInline ct ft sharedMsgId )
( messageError " x.file.acpt.inv: fileSize is bigger than allowed to send inline " )
2022-09-20 14:46:30 +01:00
else messageError " x.file.acpt.inv: fileName is different from expected "
2023-04-18 18:51:14 +04:00
assertSMPAcceptNotProhibited :: ChatItem c d -> m ()
assertSMPAcceptNotProhibited ChatItem { file = Just CIFile { fileId , fileProtocol } , content }
| fileProtocol == FPXFTP && not ( imageOrVoice content ) = throwChatError $ CEFallbackToSMPProhibited fileId
| otherwise = pure ()
where
imageOrVoice :: CIContent d -> Bool
imageOrVoice ( CISndMsgContent ( MCImage _ _ ) ) = True
imageOrVoice ( CISndMsgContent ( MCVoice _ _ ) ) = True
imageOrVoice _ = False
assertSMPAcceptNotProhibited _ = pure ()
2022-10-14 13:06:33 +01:00
checkSndInlineFTComplete :: Connection -> AgentMsgId -> m ()
checkSndInlineFTComplete conn agentMsgId = do
2023-03-30 19:45:18 +04:00
sft_ <- withStore' $ \ db -> getSndFTViaMsgDelivery db user conn agentMsgId
forM_ sft_ $ \ sft @ SndFileTransfer { fileId } -> do
ci @ ( AChatItem _ _ _ ChatItem { file } ) <- withStore $ \ db -> do
liftIO $ updateSndFileStatus db sft FSComplete
liftIO $ deleteSndFileChunks db sft
2022-10-14 13:06:33 +01:00
updateDirectCIFileStatus db user fileId CIFSSndComplete
2023-03-30 19:45:18 +04:00
case file of
Just CIFile { fileProtocol = FPXFTP } -> do
ft <- withStore $ \ db -> getFileTransferMeta db user fileId
toView $ CRSndFileCompleteXFTP user ci ft
_ -> toView $ CRSndFileComplete user ci sft
2022-10-14 13:06:33 +01:00
allowSendInline :: Integer -> Maybe InlineFileMode -> m Bool
allowSendInline fileSize = \ case
Just IFMOffer -> do
ChatConfig { fileChunkSize , inlineFiles } <- asks config
pure $ fileSize <= fileChunkSize * offerChunks inlineFiles
_ -> pure False
bFileChunk :: Contact -> SharedMsgId -> FileChunk -> MsgMeta -> m ()
bFileChunk ct sharedMsgId chunk meta = do
ft <- withStore $ \ db -> getDirectFileIdBySharedMsgId db user ct sharedMsgId >>= getRcvFileTransfer db user
receiveInlineChunk ft chunk meta
bFileChunkGroup :: GroupInfo -> SharedMsgId -> FileChunk -> MsgMeta -> m ()
bFileChunkGroup GroupInfo { groupId } sharedMsgId chunk meta = do
ft <- withStore $ \ db -> getGroupFileIdBySharedMsgId db userId groupId sharedMsgId >>= getRcvFileTransfer db user
receiveInlineChunk ft chunk meta
receiveInlineChunk :: RcvFileTransfer -> FileChunk -> MsgMeta -> m ()
2022-11-26 22:39:56 +00:00
receiveInlineChunk RcvFileTransfer { fileId , fileStatus = RFSNew } FileChunk { chunkNo } _
2022-11-27 13:54:34 +00:00
| chunkNo == 1 = throwChatError $ CEInlineFileProhibited fileId
2022-11-26 22:39:56 +00:00
| otherwise = pure ()
2023-03-16 10:49:57 +04:00
receiveInlineChunk ft @ RcvFileTransfer { fileId } chunk meta = do
2022-10-14 13:06:33 +01:00
case chunk of
2023-03-16 10:49:57 +04:00
FileChunk { chunkNo } -> when ( chunkNo == 1 ) $ startReceivingFile user fileId
2022-10-14 13:06:33 +01:00
_ -> pure ()
receiveFileChunk ft Nothing meta chunk
2022-05-11 16:18:28 +04:00
xFileCancelGroup :: GroupInfo -> GroupMember -> SharedMsgId -> MsgMeta -> m ()
2022-10-01 14:31:21 +04:00
xFileCancelGroup g @ GroupInfo { groupId } mem @ GroupMember { groupMemberId , memberId } sharedMsgId msgMeta = do
2022-05-28 19:13:07 +01:00
checkIntegrityCreateItem ( CDGroupRcv g mem ) msgMeta
2022-06-18 20:06:13 +01:00
fileId <- withStore $ \ db -> getGroupFileIdBySharedMsgId db userId groupId sharedMsgId
2022-10-01 14:31:21 +04:00
CChatItem msgDir ChatItem { chatDir } <- withStore $ \ db -> getGroupChatItemBySharedMsgId db user groupId groupMemberId sharedMsgId
2022-05-11 16:18:28 +04:00
case ( msgDir , chatDir ) of
( SMDRcv , CIGroupRcv m ) -> do
if sameMemberId memberId m
then do
2023-04-18 12:48:36 +04:00
ft <- withStore ( \ db -> getRcvFileTransfer db user fileId )
unless ( rcvFileCompleteOrCancelled ft ) $ do
2023-01-24 16:24:34 +04:00
cancelRcvFileTransfer user ft >>= mapM_ ( deleteAgentConnectionAsync user )
2023-03-29 17:18:44 +04:00
ci <- withStore $ \ db -> getChatItemByFileId db user fileId
toView $ CRRcvFileSndCancelled user ci ft
2022-10-01 14:31:21 +04:00
else messageError " x.file.cancel: group member attempted to cancel file of another member " -- shouldn't happen now that query includes group member id
2022-05-11 16:18:28 +04:00
( SMDSnd , _ ) -> messageError " x.file.cancel: group member attempted invalid file cancel "
2022-04-05 10:01:08 +04:00
2022-10-14 13:06:33 +01:00
xFileAcptInvGroup :: GroupInfo -> GroupMember -> SharedMsgId -> Maybe ConnReqInvitation -> String -> MsgMeta -> m ()
xFileAcptInvGroup g @ GroupInfo { groupId } m @ GroupMember { activeConn } sharedMsgId fileConnReq_ fName msgMeta = do
2022-05-28 19:13:07 +01:00
checkIntegrityCreateItem ( CDGroupRcv g m ) msgMeta
2022-06-18 20:06:13 +01:00
fileId <- withStore $ \ db -> getGroupFileIdBySharedMsgId db userId groupId sharedMsgId
2023-04-18 18:51:14 +04:00
( AChatItem _ _ _ ci ) <- withStore $ \ db -> getChatItemByFileId db user fileId
assertSMPAcceptNotProhibited ci
2022-12-12 16:33:07 +04:00
-- TODO check that it's not already accepted
2022-10-14 13:06:33 +01:00
ft @ FileTransferMeta { fileName , fileSize , fileInline , cancelled } <- withStore ( \ db -> getFileTransferMeta db user fileId )
2022-09-20 14:46:30 +01:00
if fName == fileName
2022-10-14 13:06:33 +01:00
then unless cancelled $ case ( fileConnReq_ , activeConn ) of
( Just fileConnReq , _ ) -> do
2023-09-10 22:40:15 +03:00
subMode <- chatReadVar subscriptionMode
2022-10-14 13:06:33 +01:00
-- receiving via a separate connection
-- [async agent commands] no continuation needed, but command should be asynchronous for stability
2023-09-10 22:40:15 +03:00
dm <- directMessage XOk
connIds <- joinAgentConnectionAsync user True fileConnReq dm subMode
withStore' $ \ db -> createSndGroupFileTransferConnection db user fileId connIds m subMode
2022-10-14 13:06:33 +01:00
( _ , Just conn ) -> do
-- receiving inline
event <- withStore $ \ db -> do
2023-04-18 18:51:14 +04:00
ci' <- updateDirectCIFileStatus db user fileId $ CIFSSndTransfer 0 1
2022-10-14 13:06:33 +01:00
sft <- liftIO $ createSndGroupInlineFT db m conn ft
2023-04-18 18:51:14 +04:00
pure $ CRSndFileStart user ci' sft
2022-10-14 13:06:33 +01:00
toView event
ifM
( allowSendInline fileSize fileInline )
( sendMemberFileInline m conn ft sharedMsgId )
( messageError " x.file.acpt.inv: fileSize is bigger than allowed to send inline " )
_ -> messageError " x.file.acpt.inv: member connection is not active "
2022-09-20 14:46:30 +01:00
else messageError " x.file.acpt.inv: fileName is different from expected "
2022-04-05 10:01:08 +04:00
2022-05-28 19:13:07 +01:00
groupMsgToView :: GroupInfo -> GroupMember -> ChatItem 'CTGroup 'MDRcv -> MsgMeta -> m ()
groupMsgToView gInfo m ci msgMeta = do
checkIntegrityCreateItem ( CDGroupRcv gInfo m ) msgMeta
2023-01-04 21:06:28 +04:00
toView $ CRNewChatItem user ( AChatItem SCTGroup SMDRcv ( GroupChat gInfo ) ci )
2022-02-22 14:05:45 +00:00
2022-07-14 22:04:23 +04:00
processGroupInvitation :: Contact -> GroupInvitation -> RcvMessage -> MsgMeta -> m ()
2023-03-22 15:58:01 +00:00
processGroupInvitation ct inv msg msgMeta = do
2023-09-06 11:41:23 +04:00
let Contact { localDisplayName = c , activeConn = Connection { peerChatVRange , customUserProfileId , groupLinkId = groupLinkId' } } = ct
2023-03-22 15:58:01 +00:00
GroupInvitation { fromMember = ( MemberIdRole fromMemId fromRole ) , invitedMember = ( MemberIdRole memId memRole ) , connRequest , groupLinkId } = inv
2022-07-14 22:04:23 +04:00
checkIntegrityCreateItem ( CDDirectRcv ct ) msgMeta
2023-02-01 13:57:39 +00:00
when ( fromRole < GRAdmin || fromRole < memRole ) $ throwChatError ( CEGroupContactRole c )
2022-01-26 21:20:08 +00:00
when ( fromMemId == memId ) $ throwChatError CEGroupDuplicateMemberId
2022-08-27 19:56:03 +04:00
-- [incognito] if direct connection with host is incognito, create membership using the same incognito profile
2022-11-03 14:46:36 +04:00
( gInfo @ GroupInfo { groupId , localDisplayName , groupProfile , membership = membership @ GroupMember { groupMemberId , memberId } } , hostId ) <- withStore $ \ db -> createGroupInvitation db user ct inv customUserProfileId
if sameGroupLinkId groupLinkId groupLinkId'
then do
2023-09-10 22:40:15 +03:00
subMode <- chatReadVar subscriptionMode
dm <- directMessage $ XGrpAcpt memberId
connIds <- joinAgentConnectionAsync user True connRequest dm subMode
2022-11-03 14:46:36 +04:00
withStore' $ \ db -> do
2023-09-12 17:59:09 +04:00
createMemberConnectionAsync db user hostId connIds ( fromJVersionRange peerChatVRange ) subMode
2022-11-03 14:46:36 +04:00
updateGroupMemberStatusById db userId hostId GSMemAccepted
updateGroupMemberStatus db userId membership GSMemAccepted
2023-01-04 21:06:28 +04:00
toView $ CRUserAcceptedGroupSent user gInfo { membership = membership { memberStatus = GSMemAccepted } } ( Just ct )
2022-11-03 14:46:36 +04:00
else do
let content = CIRcvGroupInvitation ( CIGroupInvitation { groupId , groupMemberId , localDisplayName , groupProfile , status = CIGISPending } ) memRole
2022-12-16 07:51:04 +00:00
ci <- saveRcvChatItem user ( CDDirectRcv ct ) msg msgMeta content
2022-11-03 14:46:36 +04:00
withStore' $ \ db -> setGroupInvitationChatItemId db user groupId ( chatItemId' ci )
2023-01-04 21:06:28 +04:00
toView $ CRNewChatItem user ( AChatItem SCTDirect SMDRcv ( DirectChat ct ) ci )
2023-08-16 21:21:12 +04:00
toView $ CRReceivedGroupInvitation { user , groupInfo = gInfo , contact = ct , fromMemberRole = fromRole , memberRole = memRole }
2023-03-22 15:58:01 +00:00
whenContactNtfs user ct $
showToast ( " # " <> localDisplayName <> " " <> c <> " > " ) " invited you to join the group "
2022-11-03 14:46:36 +04:00
where
sameGroupLinkId :: Maybe GroupLinkId -> Maybe GroupLinkId -> Bool
sameGroupLinkId ( Just gli ) ( Just gli' ) = gli == gli'
sameGroupLinkId _ _ = False
2021-07-12 19:00:03 +01:00
2022-05-28 19:13:07 +01:00
checkIntegrityCreateItem :: forall c . ChatTypeI c => ChatDirection c 'MDRcv -> MsgMeta -> m ()
checkIntegrityCreateItem cd MsgMeta { integrity , broker = ( _ , brokerTs ) } = case integrity of
2022-02-02 11:43:52 +00:00
MsgOk -> pure ()
2023-04-16 12:35:45 +02:00
MsgError e -> createInternalChatItem user cd ( CIRcvIntegrityError e ) ( Just brokerTs )
2022-11-01 13:26:08 +00:00
2021-08-22 15:56:36 +01:00
xInfo :: Contact -> Profile -> m ()
2022-08-18 11:35:31 +04:00
xInfo c @ Contact { profile = p } p' = unless ( fromLocalProfile p == p' ) $ do
2022-12-21 19:54:44 +04:00
c' <- withStore $ \ db ->
if userTTL == rcvTTL
then updateContactProfile db user c p'
else do
c' <- liftIO $ updateContactUserPreferences db user c ctUserPrefs'
updateContactProfile db user c' p'
2022-12-22 14:56:29 +00:00
when ( directOrUsed c' ) $ createRcvFeatureItems user c c'
2023-01-04 21:06:28 +04:00
toView $ CRContactUpdated user c c'
2022-12-21 19:54:44 +04:00
where
Contact { userPreferences = ctUserPrefs @ Preferences { timedMessages = ctUserTMPref } } = c
userTTL = prefParam $ getPreference SCFTimedMessages ctUserPrefs
Profile { preferences = rcvPrefs_ } = p'
rcvTTL = prefParam $ getPreference SCFTimedMessages rcvPrefs_
ctUserPrefs' =
let userDefault = getPreference SCFTimedMessages ( fullPreferences user )
userDefaultTTL = prefParam userDefault
ctUserTMPref' = case ctUserTMPref of
Just userTM -> Just ( userTM :: TimedMessagesPreference ) { ttl = rcvTTL }
_
| rcvTTL /= userDefaultTTL -> Just ( userDefault :: TimedMessagesPreference ) { ttl = rcvTTL }
| otherwise -> Nothing
in setPreference_ SCFTimedMessages ctUserTMPref' ctUserPrefs
2022-11-22 12:50:56 +00:00
createFeatureEnabledItems :: Contact -> m ()
2022-11-25 15:16:55 +04:00
createFeatureEnabledItems ct @ Contact { mergedPreferences } =
2022-12-13 14:52:34 +00:00
forM_ allChatFeatures $ \ ( ACF f ) -> do
2022-12-19 21:18:59 +04:00
let state = featureState $ getContactUserPreference f mergedPreferences
createInternalChatItem user ( CDDirectRcv ct ) ( uncurry ( CIRcvChatFeature $ chatFeature f ) state ) Nothing
2022-11-23 11:04:08 +00:00
createGroupFeatureItems :: GroupInfo -> GroupMember -> m ()
2022-12-19 21:18:59 +04:00
createGroupFeatureItems g @ GroupInfo { fullGroupPreferences } m =
2022-12-14 08:30:24 +00:00
forM_ allGroupFeatures $ \ ( AGF f ) -> do
2022-12-19 21:18:59 +04:00
let p = getGroupPreference f fullGroupPreferences
( _ , param ) = groupFeatureState p
createInternalChatItem user ( CDGroupRcv g m ) ( CIRcvGroupFeature ( toGroupFeature f ) ( toGroupPreference p ) param ) Nothing
2021-08-22 15:56:36 +01:00
2023-09-20 00:26:03 +04:00
xInfoProbe :: ContactOrGroupMember -> Probe -> m ()
xInfoProbe cgm2 probe =
2022-08-18 11:35:31 +04:00
-- [incognito] unless connected incognito
2023-09-20 00:26:03 +04:00
unless ( contactOrGroupMemberIncognito cgm2 ) $ do
r <- withStore' $ \ db -> matchReceivedProbe db user cgm2 probe
forM_ r $ \ case
CGMContact c1 -> probeMatch c1 cgm2 probe
CGMGroupMember _ _ -> messageWarning " xInfoProbe ignored: matched member (no probe hashes sent to members) "
2021-07-27 08:08:05 +01:00
2023-09-20 00:26:03 +04:00
-- TODO currently we send probe hashes only to contacts
2022-01-11 08:50:44 +00:00
xInfoProbeCheck :: Contact -> ProbeHash -> m ()
2022-08-18 11:35:31 +04:00
xInfoProbeCheck c1 probeHash =
-- [incognito] unless connected incognito
unless ( contactConnIncognito c1 ) $ do
2023-09-20 00:26:03 +04:00
r <- withStore' $ \ db -> matchReceivedProbeHash db user ( CGMContact c1 ) probeHash
2022-08-18 11:35:31 +04:00
forM_ r . uncurry $ probeMatch c1
2021-07-27 08:08:05 +01:00
2023-09-20 00:26:03 +04:00
probeMatch :: Contact -> ContactOrGroupMember -> Probe -> m ()
probeMatch c1 @ Contact { contactId = cId1 , profile = p1 } cgm2 probe =
case cgm2 of
CGMContact c2 @ Contact { contactId = cId2 , profile = p2 }
| cId1 /= cId2 && profilesMatch p1 p2 -> do
void . sendDirectContactMessage c1 $ XInfoProbeOk probe
mergeContacts c1 c2
| otherwise -> messageWarning " probeMatch ignored: profiles don't match or same contact id "
CGMGroupMember g m2 @ GroupMember { memberProfile = p2 , memberContactId }
| isNothing memberContactId && profilesMatch p1 p2 -> do
void . sendDirectContactMessage c1 $ XInfoProbeOk probe
connectContactToMember c1 g m2
| otherwise -> messageWarning " probeMatch ignored: profiles don't match or member already has contact "
-- TODO currently we send probe hashes only to contacts
2022-01-11 08:50:44 +00:00
xInfoProbeOk :: Contact -> Probe -> m ()
2023-09-20 00:26:03 +04:00
xInfoProbeOk c1 @ Contact { contactId = cId1 } probe =
withStore' ( \ db -> matchSentProbe db user ( CGMContact c1 ) probe ) >>= \ case
Just ( CGMContact c2 @ Contact { contactId = cId2 } )
| cId1 /= cId2 -> mergeContacts c1 c2
| otherwise -> messageWarning " xInfoProbeOk ignored: same contact id "
Just ( CGMGroupMember g m2 @ GroupMember { memberContactId } )
| isNothing memberContactId -> connectContactToMember c1 g m2
| otherwise -> messageWarning " xInfoProbeOk ignored: member already has contact "
_ -> pure ()
2021-07-27 08:08:05 +01:00
2022-05-03 10:22:35 +01:00
-- to party accepting call
xCallInv :: Contact -> CallId -> CallInvitation -> RcvMessage -> MsgMeta -> m ()
2023-04-17 11:18:04 +02:00
xCallInv ct @ Contact { contactId } callId CallInvitation { callType , callDhPubKey } msg @ RcvMessage { sharedMsgId_ } msgMeta = do
2022-05-28 19:13:07 +01:00
checkIntegrityCreateItem ( CDDirectRcv ct ) msgMeta
2023-04-17 11:18:04 +02:00
if featureAllowed SCFCalls forContact ct
then do
dhKeyPair <- if encryptedCall callType then Just <$> liftIO C . generateKeyPair' else pure Nothing
ci <- saveCallItem CISCallPending
let sharedKey = C . Key . C . dhBytes' <$> ( C . dh' <$> callDhPubKey <*> ( snd <$> dhKeyPair ) )
callState = CallInvitationReceived { peerCallType = callType , localDhPubKey = fst <$> dhKeyPair , sharedKey }
call' = Call { contactId , callId , chatItemId = chatItemId' ci , callState , callTs = chatItemTs' ci }
calls <- asks currentCalls
-- theoretically, the new call invitation for the current contact can mark the in-progress call as ended
-- (and replace it in ChatController)
-- practically, this should not happen
withStore' $ \ db -> createCall db user call' $ chatItemTs' ci
call_ <- atomically ( TM . lookupInsert contactId call' calls )
forM_ call_ $ \ call -> updateCallItemStatus user ct call WCSDisconnected Nothing
toView $ CRCallInvitation RcvCallInvitation { user , contact = ct , callType , sharedKey , callTs = chatItemTs' ci }
toView $ CRNewChatItem user $ AChatItem SCTDirect SMDRcv ( DirectChat ct ) ci
else featureRejected CFCalls
2022-05-03 10:22:35 +01:00
where
2022-12-16 07:51:04 +00:00
saveCallItem status = saveRcvChatItem user ( CDDirectRcv ct ) msg msgMeta ( CIRcvCall status 0 )
2023-04-17 11:18:04 +02:00
featureRejected f = do
ci <- saveRcvChatItem' user ( CDDirectRcv ct ) msg sharedMsgId_ msgMeta ( CIRcvChatFeatureRejected f ) Nothing Nothing False
toView $ CRNewChatItem user ( AChatItem SCTDirect SMDRcv ( DirectChat ct ) ci )
2022-05-03 10:22:35 +01:00
-- to party initiating call
xCallOffer :: Contact -> CallId -> CallOffer -> RcvMessage -> MsgMeta -> m ()
xCallOffer ct callId CallOffer { callType , rtcSession , callDhPubKey } msg msgMeta = do
msgCurrentCall ct callId " x.call.offer " msg msgMeta $
\ call -> case callState call of
CallInvitationSent { localCallType , localDhPrivKey } -> do
let sharedKey = C . Key . C . dhBytes' <$> ( C . dh' <$> callDhPubKey <*> localDhPrivKey )
callState' = CallOfferReceived { localCallType , peerCallType = callType , peerCallSession = rtcSession , sharedKey }
2022-05-18 07:01:32 +01:00
askConfirmation = encryptedCall localCallType && not ( encryptedCall callType )
2023-01-04 21:06:28 +04:00
toView CRCallOffer { user , contact = ct , callType , offer = rtcSession , sharedKey , askConfirmation }
2022-05-03 10:22:35 +01:00
pure ( Just call { callState = callState' } , Just . ACIContent SMDSnd $ CISndCall CISCallAccepted 0 )
_ -> do
msgCallStateError " x.call.offer " call
pure ( Just call , Nothing )
-- to party accepting call
xCallAnswer :: Contact -> CallId -> CallAnswer -> RcvMessage -> MsgMeta -> m ()
xCallAnswer ct callId CallAnswer { rtcSession } msg msgMeta = do
msgCurrentCall ct callId " x.call.answer " msg msgMeta $
\ call -> case callState call of
CallOfferSent { localCallType , peerCallType , localCallSession , sharedKey } -> do
let callState' = CallNegotiated { localCallType , peerCallType , localCallSession , peerCallSession = rtcSession , sharedKey }
2023-01-04 21:06:28 +04:00
toView $ CRCallAnswer user ct rtcSession
2022-05-03 10:22:35 +01:00
pure ( Just call { callState = callState' } , Just . ACIContent SMDRcv $ CIRcvCall CISCallNegotiated 0 )
_ -> do
msgCallStateError " x.call.answer " call
pure ( Just call , Nothing )
-- to any call party
xCallExtra :: Contact -> CallId -> CallExtraInfo -> RcvMessage -> MsgMeta -> m ()
xCallExtra ct callId CallExtraInfo { rtcExtraInfo } msg msgMeta = do
msgCurrentCall ct callId " x.call.extra " msg msgMeta $
\ call -> case callState call of
2022-05-07 06:40:46 +01:00
CallOfferReceived { localCallType , peerCallType , peerCallSession , sharedKey } -> do
-- TODO update the list of ice servers in peerCallSession
let callState' = CallOfferReceived { localCallType , peerCallType , peerCallSession , sharedKey }
2023-01-04 21:06:28 +04:00
toView $ CRCallExtraInfo user ct rtcExtraInfo
2022-05-07 06:40:46 +01:00
pure ( Just call { callState = callState' } , Nothing )
2022-05-03 10:22:35 +01:00
CallNegotiated { localCallType , peerCallType , localCallSession , peerCallSession , sharedKey } -> do
2022-05-07 06:40:46 +01:00
-- TODO update the list of ice servers in peerCallSession
2022-05-03 10:22:35 +01:00
let callState' = CallNegotiated { localCallType , peerCallType , localCallSession , peerCallSession , sharedKey }
2023-01-04 21:06:28 +04:00
toView $ CRCallExtraInfo user ct rtcExtraInfo
2022-05-03 10:22:35 +01:00
pure ( Just call { callState = callState' } , Nothing )
_ -> do
2022-05-04 13:31:00 +01:00
msgCallStateError " x.call.extra " call
2022-05-03 10:22:35 +01:00
pure ( Just call , Nothing )
-- to any call party
xCallEnd :: Contact -> CallId -> RcvMessage -> MsgMeta -> m ()
2022-05-07 06:40:46 +01:00
xCallEnd ct callId msg msgMeta =
2022-05-04 13:31:00 +01:00
msgCurrentCall ct callId " x.call.end " msg msgMeta $ \ Call { chatItemId } -> do
2023-01-04 21:06:28 +04:00
toView $ CRCallEnded user ct
2022-12-20 12:58:15 +00:00
( Nothing , ) <$> callStatusItemContent user ct chatItemId WCSDisconnected
2022-05-03 10:22:35 +01:00
msgCurrentCall :: Contact -> CallId -> Text -> RcvMessage -> MsgMeta -> ( Call -> m ( Maybe Call , Maybe ACIContent ) ) -> m ()
msgCurrentCall ct @ Contact { contactId = ctId' } callId' eventName RcvMessage { msgId } msgMeta action = do
2022-05-28 19:13:07 +01:00
checkIntegrityCreateItem ( CDDirectRcv ct ) msgMeta
2022-05-04 13:31:00 +01:00
calls <- asks currentCalls
atomically ( TM . lookup ctId' calls ) >>= \ case
2022-05-03 10:22:35 +01:00
Nothing -> messageError $ eventName <> " : no current call "
Just call @ Call { contactId , callId , chatItemId }
| contactId /= ctId' || callId /= callId' -> messageError $ eventName <> " : wrong contact or callId "
| otherwise -> do
2022-10-15 14:48:07 +04:00
( call_ , aciContent_ ) <- action 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
forM_ aciContent_ $ \ aciContent ->
2022-12-20 12:58:15 +00:00
updateDirectChatItemView user ct chatItemId aciContent False $ Just msgId
2022-05-03 10:22:35 +01:00
msgCallStateError :: Text -> Call -> m ()
msgCallStateError eventName Call { callState } =
messageError $ eventName <> " : wrong call state " <> T . pack ( show $ callStateTag callState )
2021-07-27 08:08:05 +01:00
mergeContacts :: Contact -> Contact -> m ()
2023-05-26 13:52:06 +04:00
mergeContacts c1 c2 = do
withStore' $ \ db -> mergeContactRecords db userId c1 c2
toView $ CRContactsMerged user c1 c2
2021-07-27 08:08:05 +01:00
2023-09-20 00:26:03 +04:00
connectContactToMember :: Contact -> GroupInfo -> GroupMember -> m ()
connectContactToMember c1 g m2 = do
withStore' $ \ db -> updateMemberContact db user c1 m2
toView $ CRMemberContactConnected user c1 g m2
2023-09-01 19:20:07 +04:00
saveConnInfo :: Connection -> ConnInfo -> m Connection
2022-10-24 14:28:58 +04:00
saveConnInfo activeConn connInfo = do
2023-09-01 19:20:07 +04:00
ChatMessage { chatVRange , chatMsgEvent } <- parseChatMessage activeConn connInfo
2023-09-06 11:41:23 +04:00
conn' <- updatePeerChatVRange activeConn chatVRange
2021-07-06 19:07:03 +01:00
case chatMsgEvent of
2022-02-08 13:04:17 +04:00
XInfo p -> do
2023-09-01 19:20:07 +04:00
ct <- withStore $ \ db -> createDirectContact db user conn' p
2023-01-04 21:06:28 +04:00
toView $ CRContactConnecting user ct
2023-09-01 19:20:07 +04:00
pure conn'
2021-07-24 18:11:04 +01:00
-- TODO show/log error, other events in SMP confirmation
2023-09-01 19:20:07 +04:00
_ -> pure conn'
2021-07-24 18:11:04 +01:00
2022-07-20 16:56:55 +04:00
xGrpMemNew :: GroupInfo -> GroupMember -> MemberInfo -> RcvMessage -> MsgMeta -> m ()
2023-09-05 20:15:50 +04:00
xGrpMemNew gInfo m memInfo @ ( MemberInfo memId memRole _ memberProfile ) msg msgMeta = do
2022-10-01 20:30:47 +01:00
checkHostRole m memRole
2022-06-18 20:06:13 +01:00
members <- withStore' $ \ db -> getGroupMembers db user gInfo
2022-01-26 16:18:27 +04:00
unless ( sameMemberId memId $ membership gInfo ) $
if isMember memId gInfo members
2021-07-24 18:11:04 +01:00
then messageError " x.grp.mem.new error: member already exists "
else do
2022-07-20 16:56:55 +04:00
newMember @ GroupMember { groupMemberId } <- withStore $ \ db -> createNewGroupMember db user gInfo memInfo GCPostMember GSMemAnnounced
2022-12-16 07:51:04 +00:00
ci <- saveRcvChatItem user ( CDGroupRcv gInfo m ) msg msgMeta ( CIRcvGroupEvent $ RGEMemberAdded groupMemberId memberProfile )
2022-07-20 16:56:55 +04:00
groupMsgToView gInfo m ci msgMeta
2023-01-04 21:06:28 +04:00
toView $ CRJoinedGroupMemberConnecting user gInfo m newMember
2021-07-24 18:11:04 +01:00
2022-09-14 19:45:21 +04:00
xGrpMemIntro :: GroupInfo -> GroupMember -> MemberInfo -> m ()
2023-09-19 18:50:10 +01:00
xGrpMemIntro gInfo @ GroupInfo { chatSettings = ChatSettings { enableNtfs } } m @ GroupMember { memberRole , localDisplayName = c } memInfo @ ( MemberInfo memId _ memberChatVRange _ ) = do
2021-07-24 18:11:04 +01:00
case memberCategory m of
GCHostMember -> do
2022-06-18 20:06:13 +01:00
members <- withStore' $ \ db -> getGroupMembers db user gInfo
2022-01-26 16:18:27 +04:00
if isMember memId gInfo members
2021-07-24 18:11:04 +01:00
then messageWarning " x.grp.mem.intro ignored: member already exists "
else do
2023-02-01 13:57:39 +00:00
when ( memberRole < GRAdmin ) $ throwChatError ( CEGroupContactRole c )
2023-09-10 22:40:15 +03:00
subMode <- chatReadVar subscriptionMode
2022-09-14 19:45:21 +04:00
-- [async agent commands] commands should be asynchronous, continuation is to send XGrpMemInv - have to remember one has completed and process on second
2023-09-10 22:40:15 +03:00
groupConnIds <- createConn subMode
2023-09-05 20:15:50 +04:00
directConnIds <- case memberChatVRange of
2023-09-10 22:40:15 +03:00
Nothing -> Just <$> createConn subMode
2023-09-06 17:48:37 +04:00
Just mcvr
2023-09-20 00:26:03 +04:00
| isCompatibleRange ( fromChatVRange mcvr ) groupNoDirectVRange -> pure Nothing
2023-09-10 22:40:15 +03:00
| otherwise -> Just <$> createConn subMode
2023-09-19 18:50:10 +01:00
let customUserProfileId = localProfileId <$> incognitoMembershipProfile gInfo
2023-09-10 22:40:15 +03:00
void $ withStore $ \ db -> createIntroReMember db user gInfo m memInfo groupConnIds directConnIds customUserProfileId subMode
2021-07-24 18:11:04 +01:00
_ -> messageError " x.grp.mem.intro can be only sent by host member "
2023-09-05 20:15:50 +04:00
where
2023-09-10 22:40:15 +03:00
createConn subMode = createAgentConnectionAsync user CFCreateConnGrpMemInv enableNtfs SCMInvitation subMode
2021-07-24 18:11:04 +01:00
2023-09-05 20:15:50 +04:00
sendXGrpMemInv :: Int64 -> Maybe ConnReqInvitation -> XGrpMemIntroCont -> m ()
2022-10-15 14:48:07 +04:00
sendXGrpMemInv hostConnId directConnReq XGrpMemIntroCont { groupId , groupMemberId , memberId , groupConnReq } = do
2022-09-14 19:45:21 +04:00
hostConn <- withStore $ \ db -> getConnectionById db user hostConnId
let msg = XGrpMemInv memberId IntroInvitation { groupConnReq , directConnReq }
void $ sendDirectMessage hostConn msg ( GroupId groupId )
withStore' $ \ db -> updateGroupMemberStatusById db userId groupMemberId GSMemIntroInvited
2022-01-26 16:18:27 +04:00
xGrpMemInv :: GroupInfo -> GroupMember -> MemberId -> IntroInvitation -> m ()
2022-10-15 14:48:07 +04:00
xGrpMemInv gInfo @ GroupInfo { groupId } m memId introInv = do
2021-07-24 18:11:04 +01:00
case memberCategory m of
GCInviteeMember -> do
2022-06-18 20:06:13 +01:00
members <- withStore' $ \ db -> getGroupMembers db user gInfo
2022-01-26 16:18:27 +04:00
case find ( sameMemberId memId ) members of
2022-03-28 20:35:57 +04:00
Nothing -> messageError " x.grp.mem.inv error: referenced member does not exist "
2021-07-24 18:11:04 +01:00
Just reMember -> do
2022-06-18 20:06:13 +01:00
GroupMemberIntro { introId } <- withStore $ \ db -> saveIntroInvitation db reMember m introInv
2023-01-13 14:19:21 +04:00
void . sendGroupMessage' user [ reMember ] ( XGrpMemFwd ( memberInfo m ) introInv ) groupId ( Just introId ) $
2022-10-15 14:48:07 +04:00
withStore' $ \ db -> updateIntroStatus db introId GMIntroInvForwarded
2021-07-24 18:11:04 +01:00
_ -> messageError " x.grp.mem.inv can be only sent by invitee member "
2022-01-26 16:18:27 +04:00
xGrpMemFwd :: GroupInfo -> GroupMember -> MemberInfo -> IntroInvitation -> m ()
2023-09-05 20:15:50 +04:00
xGrpMemFwd gInfo @ GroupInfo { membership , chatSettings = ChatSettings { enableNtfs } } m memInfo @ ( MemberInfo memId memRole memberChatVRange _ ) introInv @ IntroInvitation { groupConnReq , directConnReq } = do
2022-10-01 20:30:47 +01:00
checkHostRole m memRole
2022-06-18 20:06:13 +01:00
members <- withStore' $ \ db -> getGroupMembers db user gInfo
2022-01-26 16:18:27 +04:00
toMember <- case find ( sameMemberId memId ) members of
2021-07-24 18:11:04 +01:00
-- TODO if the missed messages are correctly sent as soon as there is connection before anything else is sent
-- the situation when member does not exist is an error
-- member receiving x.grp.mem.fwd should have also received x.grp.mem.new prior to that.
-- For now, this branch compensates for the lack of delayed message delivery.
2022-06-18 20:06:13 +01:00
Nothing -> withStore $ \ db -> createNewGroupMember db user gInfo memInfo GCPostMember GSMemAnnounced
2021-07-24 18:11:04 +01:00
Just m' -> pure m'
2022-06-18 20:06:13 +01:00
withStore' $ \ db -> saveMemberInvitation db toMember introInv
2023-09-10 22:40:15 +03:00
subMode <- chatReadVar subscriptionMode
2022-08-18 11:35:31 +04:00
-- [incognito] send membership incognito profile, create direct connection as incognito
2023-09-10 21:11:35 +01:00
dm <- directMessage $ XGrpMemInfo membership . memberId ( fromLocalProfile $ memberProfile membership )
2022-09-14 19:45:21 +04:00
-- [async agent commands] no continuation needed, but commands should be asynchronous for stability
2023-09-10 22:40:15 +03:00
groupConnIds <- joinAgentConnectionAsync user enableNtfs groupConnReq dm subMode
directConnIds <- forM directConnReq $ \ dcr -> joinAgentConnectionAsync user enableNtfs dcr dm subMode
2023-09-19 18:50:10 +01:00
let customUserProfileId = localProfileId <$> incognitoMembershipProfile gInfo
2023-09-05 20:15:50 +04:00
mcvr = maybe chatInitialVRange fromChatVRange memberChatVRange
2023-09-10 22:40:15 +03:00
withStore' $ \ db -> createIntroToMemberContact db user m toMember mcvr groupConnIds directConnIds customUserProfileId subMode
2021-07-06 19:07:03 +01:00
2022-10-03 09:00:47 +01:00
xGrpMemRole :: GroupInfo -> GroupMember -> MemberId -> GroupMemberRole -> RcvMessage -> MsgMeta -> m ()
xGrpMemRole gInfo @ GroupInfo { membership } m @ GroupMember { memberRole = senderRole } memId memRole msg msgMeta
2023-08-25 04:56:37 +08:00
| membership . memberId == memId =
2022-10-15 14:48:07 +04:00
let gInfo' = gInfo { membership = membership { memberRole = memRole } }
in changeMemberRole gInfo' membership $ RGEUserRole memRole
2022-10-03 09:00:47 +01:00
| otherwise = do
2022-10-15 14:48:07 +04:00
members <- withStore' $ \ db -> getGroupMembers db user gInfo
case find ( sameMemberId memId ) members of
Just member -> changeMemberRole gInfo member $ RGEMemberRole ( groupMemberId' member ) ( fromLocalProfile $ memberProfile member ) memRole
_ -> messageError " x.grp.mem.role with unknown member ID "
2022-10-03 09:00:47 +01:00
where
changeMemberRole gInfo' member @ GroupMember { memberRole = fromRole } gEvent
| senderRole < GRAdmin || senderRole < fromRole = messageError " x.grp.mem.role with insufficient member permissions "
| otherwise = do
2022-10-15 14:48:07 +04:00
withStore' $ \ db -> updateGroupMemberRole db user member memRole
2022-12-16 07:51:04 +00:00
ci <- saveRcvChatItem user ( CDGroupRcv gInfo m ) msg msgMeta ( CIRcvGroupEvent gEvent )
2022-10-15 14:48:07 +04:00
groupMsgToView gInfo m ci msgMeta
2023-01-04 21:06:28 +04:00
toView CRMemberRole { user , groupInfo = gInfo' , byMember = m , member = member { memberRole = memRole } , fromRole , toRole = memRole }
2022-10-03 09:00:47 +01:00
2022-10-01 20:30:47 +01:00
checkHostRole :: GroupMember -> GroupMemberRole -> m ()
checkHostRole GroupMember { memberRole , localDisplayName } memRole =
2023-02-01 13:57:39 +00:00
when ( memberRole < GRAdmin || memberRole < memRole ) $ throwChatError ( CEGroupContactRole localDisplayName )
2022-10-01 20:30:47 +01:00
2022-07-20 16:56:55 +04:00
xGrpMemDel :: GroupInfo -> GroupMember -> MemberId -> RcvMessage -> MsgMeta -> m ()
2022-10-03 09:00:47 +01:00
xGrpMemDel gInfo @ GroupInfo { membership } m @ GroupMember { memberRole = senderRole } memId msg msgMeta = do
2022-06-18 20:06:13 +01:00
members <- withStore' $ \ db -> getGroupMembers db user gInfo
2023-08-25 04:56:37 +08:00
if membership . memberId == memId
2022-10-03 09:00:47 +01:00
then checkRole membership $ do
2023-02-01 13:57:39 +00:00
deleteGroupLinkIfExists user gInfo
2022-12-06 17:12:39 +04:00
-- member records are not deleted to keep history
2023-01-24 16:24:34 +04:00
deleteMembersConnections user members
2022-12-06 17:12:39 +04:00
withStore' $ \ db -> updateGroupMemberStatus db userId membership GSMemRemoved
deleteMemberItem RGEUserDeleted
2023-01-04 21:06:28 +04:00
toView $ CRDeletedMemberUser user gInfo { membership = membership { memberStatus = GSMemRemoved } } m
2022-01-11 08:50:44 +00:00
else case find ( sameMemberId memId ) members of
2021-08-02 20:10:24 +01:00
Nothing -> messageError " x.grp.mem.del with unknown member ID "
2022-10-03 09:00:47 +01:00
Just member @ GroupMember { groupMemberId , memberProfile } ->
checkRole member $ do
2023-02-08 21:23:53 +04:00
-- ? prohibit deleting member if it's the sender - sender should use x.grp.leave
2022-10-03 09:00:47 +01:00
deleteMemberConnection user member
2022-12-06 17:12:39 +04:00
-- undeleted "member connected" chat item will prevent deletion of member record
deleteOrUpdateMemberRecord user member
deleteMemberItem $ RGEMemberDeleted groupMemberId ( fromLocalProfile memberProfile )
2023-01-04 21:06:28 +04:00
toView $ CRDeletedMember user gInfo m member { memberStatus = GSMemRemoved }
2022-10-03 09:00:47 +01:00
where
checkRole GroupMember { memberRole } a
| senderRole < GRAdmin || senderRole < memberRole =
2022-10-15 14:48:07 +04:00
messageError " x.grp.mem.del with insufficient member permissions "
2022-10-03 09:00:47 +01:00
| otherwise = a
2022-12-06 17:12:39 +04:00
deleteMemberItem gEvent = do
2022-12-16 07:51:04 +00:00
ci <- saveRcvChatItem user ( CDGroupRcv gInfo m ) msg msgMeta ( CIRcvGroupEvent gEvent )
2022-10-03 09:00:47 +01:00
groupMsgToView gInfo m ci msgMeta
2021-08-02 20:10:24 +01:00
2022-01-11 08:50:44 +00:00
sameMemberId :: MemberId -> GroupMember -> Bool
sameMemberId memId GroupMember { memberId } = memId == memberId
2022-07-20 16:56:55 +04:00
xGrpLeave :: GroupInfo -> GroupMember -> RcvMessage -> MsgMeta -> m ()
xGrpLeave gInfo m msg msgMeta = do
2022-09-30 16:18:43 +04:00
deleteMemberConnection user m
2022-12-06 17:12:39 +04:00
-- member record is not deleted to allow creation of "member left" chat item
2022-06-18 20:06:13 +01:00
withStore' $ \ db -> updateGroupMemberStatus db userId m GSMemLeft
2022-12-16 07:51:04 +00:00
ci <- saveRcvChatItem user ( CDGroupRcv gInfo m ) msg msgMeta ( CIRcvGroupEvent RGEMemberLeft )
2022-07-20 16:56:55 +04:00
groupMsgToView gInfo m ci msgMeta
2023-01-04 21:06:28 +04:00
toView $ CRLeftMember user gInfo m { memberStatus = GSMemLeft }
2021-08-02 20:10:24 +01:00
2022-07-20 16:56:55 +04:00
xGrpDel :: GroupInfo -> GroupMember -> RcvMessage -> MsgMeta -> m ()
xGrpDel gInfo @ GroupInfo { membership } m @ GroupMember { memberRole } msg msgMeta = do
2023-02-08 07:08:53 +00:00
when ( memberRole /= GROwner ) $ throwChatError $ CEGroupUserRole gInfo GROwner
2022-06-18 20:06:13 +01:00
ms <- withStore' $ \ db -> do
members <- getGroupMembers db user gInfo
2022-07-20 16:56:55 +04:00
updateGroupMemberStatus db userId membership GSMemGroupDeleted
2021-08-02 20:10:24 +01:00
pure members
2022-12-06 17:12:39 +04:00
-- member records are not deleted to keep history
2023-01-24 16:24:34 +04:00
deleteMembersConnections user ms
2022-12-16 07:51:04 +00:00
ci <- saveRcvChatItem user ( CDGroupRcv gInfo m ) msg msgMeta ( CIRcvGroupEvent RGEGroupDeleted )
2022-07-20 16:56:55 +04:00
groupMsgToView gInfo m ci msgMeta
2023-01-04 21:06:28 +04:00
toView $ CRGroupDeleted user gInfo { membership = membership { memberStatus = GSMemGroupDeleted } } m
2021-08-02 20:10:24 +01:00
2022-07-29 19:04:32 +01:00
xGrpInfo :: GroupInfo -> GroupMember -> GroupProfile -> RcvMessage -> MsgMeta -> m ()
2022-11-23 11:04:08 +00:00
xGrpInfo g @ GroupInfo { groupProfile = p } m @ GroupMember { memberRole } p' msg msgMeta
2022-07-29 19:04:32 +01:00
| memberRole < GROwner = messageError " x.grp.info with insufficient member permissions "
2022-11-23 11:04:08 +00:00
| otherwise = unless ( p == p' ) $ do
2022-10-15 14:48:07 +04:00
g' <- withStore $ \ db -> updateGroupProfile db user g p'
2023-01-04 21:06:28 +04:00
toView $ CRGroupUpdated user g g' ( Just m )
2022-11-23 11:04:08 +00:00
let cd = CDGroupRcv g' m
unless ( sameGroupProfileInfo p p' ) $ do
2022-12-16 07:51:04 +00:00
ci <- saveRcvChatItem user cd msg msgMeta ( CIRcvGroupEvent $ RGEGroupUpdated p' )
2022-11-23 11:04:08 +00:00
groupMsgToView g' m ci msgMeta
2022-12-19 21:18:59 +04:00
createGroupFeatureChangedItems user cd CIRcvGroupFeature g g'
2022-10-14 13:06:33 +01:00
2023-09-16 17:55:48 +04:00
xGrpDirectInv :: GroupInfo -> GroupMember -> Connection -> ConnReqInvitation -> Maybe MsgContent -> RcvMessage -> MsgMeta -> m ()
xGrpDirectInv g m mConn connReq mContent_ msg msgMeta = do
unless ( groupFeatureAllowed SGFDirectMessages g ) $ messageError " x.grp.direct.inv: direct messages not allowed "
let GroupMember { memberContactId } = m
subMode <- chatReadVar subscriptionMode
case memberContactId of
Nothing -> createNewContact subMode
Just mContactId -> do
mCt <- withStore $ \ db -> getContact db user mContactId
let Contact { activeConn = Connection { connId } , contactGrpInvSent } = mCt
if contactGrpInvSent
then do
ownConnReq <- withStore $ \ db -> getConnReqInv db connId
-- in case both members sent x.grp.direct.inv before receiving other's for processing,
-- only the one who received greater connReq joins, the other creates items and waits for confirmation
if strEncode connReq > strEncode ownConnReq
then joinExistingContact subMode mCt
else createItems mCt m
else joinExistingContact subMode mCt
where
joinExistingContact subMode mCt = do
connIds <- joinConn subMode
mCt' <- withStore' $ \ db -> updateMemberContactInvited db user connIds g mConn mCt subMode
createItems mCt' m
securityCodeChanged mCt'
createNewContact subMode = do
connIds <- joinConn subMode
-- [incognito] reuse membership incognito profile
( mCt' , m' ) <- withStore' $ \ db -> createMemberContactInvited db user connIds g m mConn subMode
createItems mCt' m'
joinConn subMode = do
2023-09-19 18:50:10 +01:00
-- TODO send user's profile for this group membership
2023-09-16 17:55:48 +04:00
dm <- directMessage XOk
joinAgentConnectionAsync user True connReq dm subMode
createItems mCt' m' = do
checkIntegrityCreateItem ( CDGroupRcv g m' ) msgMeta
createInternalChatItem user ( CDGroupRcv g m' ) ( CIRcvGroupEvent RGEMemberCreatedContact ) Nothing
toView $ CRNewMemberContactReceivedInv user mCt' g m'
forM_ mContent_ $ \ mc -> do
ci <- saveRcvChatItem user ( CDDirectRcv mCt' ) msg msgMeta ( CIRcvMsgContent mc )
toView $ CRNewChatItem user ( AChatItem SCTDirect SMDRcv ( DirectChat mCt' ) ci )
securityCodeChanged ct = do
toView $ CRContactVerificationReset user ct
createInternalChatItem user ( CDDirectRcv ct ) ( CIRcvConnEvent RCEVerificationCodeReset ) Nothing
2023-07-13 23:48:25 +01:00
directMsgReceived :: Contact -> Connection -> MsgMeta -> NonEmpty MsgReceipt -> m ()
2023-07-26 14:49:35 +04:00
directMsgReceived ct conn @ Connection { connId } msgMeta msgRcpts = do
2023-07-13 23:48:25 +01:00
checkIntegrityCreateItem ( CDDirectRcv ct ) msgMeta
forM_ msgRcpts $ \ MsgReceipt { agentMsgId , msgRcptStatus } -> do
withStore $ \ db -> createSndMsgDeliveryEvent db connId agentMsgId $ MDSSndRcvd msgRcptStatus
2023-07-26 14:49:35 +04:00
updateDirectItemStatus ct conn agentMsgId $ CISSndRcvd msgRcptStatus SSPComplete
2023-07-13 23:48:25 +01:00
groupMsgReceived :: GroupInfo -> GroupMember -> Connection -> MsgMeta -> NonEmpty MsgReceipt -> m ()
2023-07-26 14:49:35 +04:00
groupMsgReceived gInfo m conn @ Connection { connId } msgMeta msgRcpts = do
2023-07-13 23:48:25 +01:00
checkIntegrityCreateItem ( CDGroupRcv gInfo m ) msgMeta
2023-07-26 14:49:35 +04:00
forM_ msgRcpts $ \ MsgReceipt { agentMsgId , msgRcptStatus } -> do
2023-07-13 23:48:25 +01:00
withStore $ \ db -> createSndMsgDeliveryEvent db connId agentMsgId $ MDSSndRcvd msgRcptStatus
2023-07-26 14:49:35 +04:00
updateGroupItemStatus gInfo m conn agentMsgId $ CISSndRcvd msgRcptStatus SSPComplete
updateDirectItemStatus :: Contact -> Connection -> AgentMsgId -> CIStatus 'MDSnd -> m ()
updateDirectItemStatus ct @ Contact { contactId } Connection { connId } msgId newStatus =
withStore' ( \ db -> getDirectChatItemByAgentMsgId db user contactId connId msgId ) >>= \ case
Just ( CChatItem SMDSnd ChatItem { meta = CIMeta { itemStatus = CISSndRcvd _ _ } } ) -> pure ()
Just ( CChatItem SMDSnd ChatItem { meta = CIMeta { itemId , itemStatus } } )
| itemStatus == newStatus -> pure ()
| otherwise -> do
chatItem <- withStore $ \ db -> updateDirectChatItemStatus db user contactId itemId newStatus
toView $ CRChatItemStatusUpdated user ( AChatItem SCTDirect SMDSnd ( DirectChat ct ) chatItem )
_ -> pure ()
updateGroupMemSndStatus :: ChatItemId -> GroupMemberId -> CIStatus 'MDSnd -> m Bool
updateGroupMemSndStatus itemId groupMemberId newStatus =
runExceptT ( withStore $ \ db -> getGroupSndStatus db itemId groupMemberId ) >>= \ case
Right ( CISSndRcvd _ _ ) -> pure False
Right memStatus
| memStatus == newStatus -> pure False
| otherwise -> withStore' ( \ db -> updateGroupSndStatus db itemId groupMemberId newStatus ) $> True
_ -> pure False
updateGroupItemStatus :: GroupInfo -> GroupMember -> Connection -> AgentMsgId -> CIStatus 'MDSnd -> m ()
updateGroupItemStatus gInfo @ GroupInfo { groupId } GroupMember { groupMemberId } Connection { connId } msgId newMemStatus =
withStore' ( \ db -> getGroupChatItemByAgentMsgId db user groupId connId msgId ) >>= \ case
Just ( CChatItem SMDSnd ChatItem { meta = CIMeta { itemStatus = CISSndRcvd _ SSPComplete } } ) -> pure ()
Just ( CChatItem SMDSnd ChatItem { meta = CIMeta { itemId , itemStatus } } ) -> do
memStatusChanged <- updateGroupMemSndStatus itemId groupMemberId newMemStatus
when memStatusChanged $ do
memStatusCounts <- withStore' ( ` getGroupSndStatusCounts ` itemId )
let newStatus = membersGroupItemStatus memStatusCounts
when ( newStatus /= itemStatus ) $ do
chatItem <- withStore $ \ db -> updateGroupChatItemStatus db user groupId itemId newStatus
toView $ CRChatItemStatusUpdated user ( AChatItem SCTGroup SMDSnd ( GroupChat gInfo ) chatItem )
_ -> pure ()
2023-07-13 23:48:25 +01:00
2023-09-06 11:41:23 +04:00
updatePeerChatVRange :: ChatMonad m => Connection -> VersionRange -> m Connection
2023-09-12 17:59:09 +04:00
updatePeerChatVRange conn @ Connection { connId , peerChatVRange } msgChatVRange = do
let jMsgChatVRange = JVersionRange msgChatVRange
if jMsgChatVRange /= peerChatVRange
then do
withStore' $ \ db -> setPeerChatVRange db connId msgChatVRange
pure conn { peerChatVRange = jMsgChatVRange }
else pure conn
2023-09-01 19:20:07 +04:00
2023-04-14 15:32:12 +04:00
parseFileDescription :: ( ChatMonad m , FilePartyI p ) => Text -> m ( ValidFileDescription p )
parseFileDescription =
2023-03-14 11:42:44 +04:00
liftEither . first ( ChatError . CEInvalidFileDescription ) . ( strDecode . encodeUtf8 )
2022-10-14 13:06:33 +01:00
sendDirectFileInline :: ChatMonad m => Contact -> FileTransferMeta -> SharedMsgId -> m ()
sendDirectFileInline ct ft sharedMsgId = do
msgDeliveryId <- sendFileInline_ ft sharedMsgId $ sendDirectContactMessage ct
withStore' $ \ db -> updateSndDirectFTDelivery db ct ft msgDeliveryId
sendMemberFileInline :: ChatMonad m => GroupMember -> Connection -> FileTransferMeta -> SharedMsgId -> m ()
sendMemberFileInline m @ GroupMember { groupId } conn ft sharedMsgId = do
msgDeliveryId <- sendFileInline_ ft sharedMsgId $ \ msg -> sendDirectMessage conn msg $ GroupId groupId
withStore' $ \ db -> updateSndGroupFTDelivery db m conn ft msgDeliveryId
sendFileInline_ :: ChatMonad m => FileTransferMeta -> SharedMsgId -> ( ChatMsgEvent 'Binary -> m ( SndMessage , Int64 ) ) -> m Int64
sendFileInline_ FileTransferMeta { filePath , chunkSize } sharedMsgId sendMsg =
sendChunks 1 =<< liftIO . B . readFile =<< toFSFilePath filePath
where
sendChunks chunkNo bytes = do
let ( chunk , rest ) = B . splitAt chSize bytes
( _ , msgDeliveryId ) <- sendMsg $ BFileChunk sharedMsgId $ FileChunk chunkNo chunk
if B . null rest
then pure msgDeliveryId
else sendChunks ( chunkNo + 1 ) rest
chSize = fromIntegral chunkSize
2023-05-26 17:36:06 +04:00
parseChatMessage :: ChatMonad m => Connection -> ByteString -> m ( ChatMessage 'Json )
parseChatMessage conn = parseChatMessage_ conn Nothing
2023-01-31 11:07:48 +00:00
{- # INLINE parseChatMessage # -}
2022-07-29 19:04:32 +01:00
2023-05-26 17:36:06 +04:00
parseAChatMessage :: ChatMonad m => Connection -> MsgMeta -> ByteString -> m AChatMessage
parseAChatMessage conn msgMeta = parseChatMessage_ conn ( Just msgMeta )
2023-01-31 11:07:48 +00:00
{- # INLINE parseAChatMessage # -}
2023-05-26 17:36:06 +04:00
parseChatMessage_ :: ( ChatMonad m , StrEncoding s ) => Connection -> Maybe MsgMeta -> ByteString -> m s
parseChatMessage_ conn msgMeta s = liftEither . first ( ChatError . errType ) $ strDecode s
where
errType = CEInvalidChatMessage conn ( msgMetaToJson <$> msgMeta ) ( safeDecodeUtf8 s )
2021-12-29 23:11:55 +04:00
2022-05-05 10:37:53 +01:00
sendFileChunk :: ChatMonad m => User -> SndFileTransfer -> m ()
2023-01-24 16:24:34 +04:00
sendFileChunk user ft @ SndFileTransfer { fileId , fileStatus , agentConnId = AgentConnId acId } =
2021-09-04 07:32:56 +01:00
unless ( fileStatus == FSComplete || fileStatus == FSCancelled ) $
2022-06-18 20:06:13 +01:00
withStore' ( ` createSndFileChunk ` ft ) >>= \ case
2021-09-04 07:32:56 +01:00
Just chunkNo -> sendFileChunkNo ft chunkNo
Nothing -> do
2022-06-18 20:06:13 +01:00
ci <- withStore $ \ db -> do
liftIO $ updateSndFileStatus db ft FSComplete
liftIO $ deleteSndFileChunks db ft
updateDirectCIFileStatus db user fileId CIFSSndComplete
2023-01-04 21:06:28 +04:00
toView $ CRSndFileComplete user ci ft
2021-09-04 07:32:56 +01:00
closeFileHandle fileId sndFiles
2023-01-24 16:24:34 +04:00
deleteAgentConnectionAsync user acId
2021-09-04 07:32:56 +01:00
sendFileChunkNo :: ChatMonad m => SndFileTransfer -> Integer -> m ()
2022-01-26 16:18:27 +04:00
sendFileChunkNo ft @ SndFileTransfer { agentConnId = AgentConnId acId } chunkNo = do
2022-01-11 12:41:38 +00:00
chunkBytes <- readFileChunk ft chunkNo
2022-06-07 14:14:54 +01:00
msgId <- withAgent $ \ a -> sendMessage a acId SMP . noMsgFlags $ smpEncode FileChunk { chunkNo , chunkBytes }
2022-06-18 20:06:13 +01:00
withStore' $ \ db -> updateSndFileChunkMsg db ft chunkNo msgId
2021-09-04 07:32:56 +01:00
readFileChunk :: ChatMonad m => SndFileTransfer -> Integer -> m ByteString
2022-04-15 09:36:38 +04:00
readFileChunk SndFileTransfer { fileId , filePath , chunkSize } chunkNo = do
fsFilePath <- toFSFilePath filePath
2023-07-09 23:24:38 +01:00
read_ fsFilePath ` catchThrow ` ( ChatError . CEFileRead filePath . show )
2021-09-04 07:32:56 +01:00
where
2022-04-15 09:36:38 +04:00
read_ fsFilePath = do
h <- getFileHandle fileId fsFilePath sndFiles ReadMode
2021-09-04 07:32:56 +01:00
pos <- hTell h
let pos' = ( chunkNo - 1 ) * chunkSize
when ( pos /= pos' ) $ hSeek h AbsoluteSeek pos'
liftIO . B . hGet h $ fromInteger chunkSize
2022-01-11 12:41:38 +00:00
parseFileChunk :: ChatMonad m => ByteString -> m FileChunk
2022-10-14 13:06:33 +01:00
parseFileChunk = liftEither . first ( ChatError . CEFileRcvChunk ) . smpDecode
2021-09-04 07:32:56 +01:00
appendFileChunk :: ChatMonad m => RcvFileTransfer -> Integer -> ByteString -> m ()
appendFileChunk ft @ RcvFileTransfer { fileId , fileStatus } chunkNo chunk =
case fileStatus of
2022-05-12 17:37:09 +04:00
RFSConnected RcvFileInfo { filePath } -> append_ filePath
-- sometimes update of file transfer status to FSConnected
-- doesn't complete in time before MSG with first file chunk
RFSAccepted RcvFileInfo { filePath } -> append_ filePath
2021-09-04 07:32:56 +01:00
RFSCancelled _ -> pure ()
2022-01-26 21:20:08 +00:00
_ -> throwChatError $ CEFileInternal " receiving file transfer not in progress "
2021-09-04 07:32:56 +01:00
where
2022-05-12 17:37:09 +04:00
append_ filePath = do
fsFilePath <- toFSFilePath filePath
h <- getFileHandle fileId fsFilePath rcvFiles AppendMode
2023-07-09 23:24:38 +01:00
liftIO ( B . hPut h chunk >> hFlush h ) ` catchThrow ` ( ChatError . CEFileWrite filePath . show )
withStore' $ \ db -> updatedRcvFileChunkStored db ft chunkNo
2021-09-04 07:32:56 +01:00
getFileHandle :: ChatMonad m => Int64 -> FilePath -> ( ChatController -> TVar ( Map Int64 Handle ) ) -> IOMode -> m Handle
getFileHandle fileId filePath files ioMode = do
fs <- asks files
h_ <- M . lookup fileId <$> readTVarIO fs
maybe ( newHandle fs ) pure h_
where
newHandle fs = do
2023-07-09 23:24:38 +01:00
h <- openFile filePath ioMode ` catchThrow ` ( ChatError . CEFileInternal . show )
2021-09-04 07:32:56 +01:00
atomically . modifyTVar fs $ M . insert fileId h
pure h
isFileActive :: ChatMonad m => Int64 -> ( ChatController -> TVar ( Map Int64 Handle ) ) -> m Bool
isFileActive fileId files = do
fs <- asks files
isJust . M . lookup fileId <$> readTVarIO fs
2023-01-24 16:24:34 +04:00
cancelRcvFileTransfer :: ChatMonad m => User -> RcvFileTransfer -> m ( Maybe ConnId )
2023-03-21 15:21:14 +04:00
cancelRcvFileTransfer user ft @ RcvFileTransfer { fileId , xftpRcvFile , rcvFileInline } =
2023-07-09 23:24:38 +01:00
cancel' ` catchChatError ` ( \ e -> toView ( CRChatError ( Just user ) e ) $> fileConnId )
2023-01-24 16:24:34 +04:00
where
cancel' = do
closeFileHandle fileId rcvFiles
withStore' $ \ db -> do
updateFileCancelled db user fileId CIFSRcvCancelled
2023-03-16 10:49:57 +04:00
updateRcvFileStatus db fileId FSCancelled
2023-01-24 16:24:34 +04:00
deleteRcvFileChunks db ft
2023-03-21 15:21:14 +04:00
case xftpRcvFile of
Just XFTPRcvFile { agentRcvFileId = Just ( AgentRcvFileId aFileId ) , agentRcvFileDeleted } ->
2023-04-25 15:46:00 +04:00
unless agentRcvFileDeleted $ agentXFTPDeleteRcvFile aFileId fileId
2023-03-21 15:21:14 +04:00
_ -> pure ()
2023-01-24 16:24:34 +04:00
pure fileConnId
2023-03-21 15:21:14 +04:00
fileConnId = if isNothing xftpRcvFile && isNothing rcvFileInline then liveRcvFileTransferConnId ft else Nothing
2021-09-04 07:32:56 +01:00
2023-01-24 16:24:34 +04:00
cancelSndFile :: ChatMonad m => User -> FileTransferMeta -> [ SndFileTransfer ] -> Bool -> m [ ConnId ]
2023-03-21 15:21:14 +04:00
cancelSndFile user FileTransferMeta { fileId , xftpSndFile } fts sendCancel = do
2023-01-24 16:24:34 +04:00
withStore' ( \ db -> updateFileCancelled db user fileId CIFSSndCancelled )
2023-07-09 23:24:38 +01:00
` catchChatError ` ( toView . CRChatError ( Just user ) )
2023-03-21 15:21:14 +04:00
case xftpSndFile of
Nothing ->
catMaybes <$> forM fts ( \ ft -> cancelSndFileTransfer user ft sendCancel )
2023-04-14 15:32:12 +04:00
Just xsf -> do
2023-03-21 15:21:14 +04:00
forM_ fts ( \ ft -> cancelSndFileTransfer user ft False )
2023-07-09 23:24:38 +01:00
agentXFTPDeleteSndFileRemote user xsf fileId ` catchChatError ` ( toView . CRChatError ( Just user ) )
2023-03-21 15:21:14 +04:00
pure []
2023-01-24 16:24:34 +04:00
cancelSndFileTransfer :: ChatMonad m => User -> SndFileTransfer -> Bool -> m ( Maybe ConnId )
2023-02-01 00:01:22 +00:00
cancelSndFileTransfer user @ User { userId } ft @ SndFileTransfer { fileId , connId , agentConnId = AgentConnId acId , fileStatus , fileInline } sendCancel =
2023-01-24 16:24:34 +04:00
if fileStatus == FSCancelled || fileStatus == FSComplete
then pure Nothing
2023-07-09 23:24:38 +01:00
else cancel' ` catchChatError ` ( \ e -> toView ( CRChatError ( Just user ) e ) $> fileConnId )
2023-01-24 16:24:34 +04:00
where
cancel' = do
withStore' $ \ db -> do
updateSndFileStatus db ft FSCancelled
deleteSndFileChunks db ft
2023-02-01 00:01:22 +00:00
when sendCancel $ case fileInline of
Just _ -> do
( sharedMsgId , conn ) <- withStore $ \ db -> ( , ) <$> getSharedMsgIdByFileId db userId fileId <*> getConnectionById db user connId
void . sendDirectMessage conn ( BFileChunk sharedMsgId FileChunkCancel ) $ ConnectionId connId
_ -> withAgent $ \ a -> void . sendMessage a acId SMP . noMsgFlags $ smpEncode FileChunkCancel
2023-01-24 16:24:34 +04:00
pure fileConnId
2023-03-21 15:21:14 +04:00
fileConnId = if isNothing fileInline then Just acId else Nothing
2021-09-04 07:32:56 +01:00
closeFileHandle :: ChatMonad m => Int64 -> ( ChatController -> TVar ( Map Int64 Handle ) ) -> m ()
closeFileHandle fileId files = do
fs <- asks files
h_ <- atomically . stateTVar fs $ \ m -> ( M . lookup fileId m , M . delete fileId m )
2023-07-09 23:24:38 +01:00
liftIO $ mapM_ hClose h_ ` catchAll_ ` pure ()
2021-09-04 07:32:56 +01:00
2022-01-26 21:20:08 +00:00
throwChatError :: ChatMonad m => ChatErrorType -> m a
throwChatError = throwError . ChatError
2021-08-02 20:10:24 +01:00
2023-01-24 16:24:34 +04:00
deleteMembersConnections :: ChatMonad m => User -> [ GroupMember ] -> m ()
deleteMembersConnections user members = do
2023-06-12 13:45:39 +04:00
let memberConns =
filter ( \ Connection { connStatus } -> connStatus /= ConnDeleted ) $
mapMaybe ( \ GroupMember { activeConn } -> activeConn ) members
2023-01-24 16:24:34 +04:00
deleteAgentConnectionsAsync user $ map aConnId memberConns
forM_ memberConns $ \ conn -> withStore' $ \ db -> updateConnectionStatus db conn ConnDeleted
2022-09-30 16:18:43 +04:00
deleteMemberConnection :: ChatMonad m => User -> GroupMember -> m ()
deleteMemberConnection user GroupMember { activeConn } = do
forM_ activeConn $ \ conn -> do
2023-01-24 16:24:34 +04:00
deleteAgentConnectionAsync user $ aConnId conn
2022-09-30 16:18:43 +04:00
withStore' $ \ db -> updateConnectionStatus db conn ConnDeleted
2022-10-01 15:19:41 +01:00
2022-12-06 17:12:39 +04:00
deleteOrUpdateMemberRecord :: ChatMonad m => User -> GroupMember -> m ()
deleteOrUpdateMemberRecord user @ User { userId } member =
withStore' $ \ db ->
checkGroupMemberHasItems db user member >>= \ case
Just _ -> updateGroupMemberStatus db userId member GSMemRemoved
Nothing -> deleteGroupMember db user member
2022-10-14 13:06:33 +01:00
sendDirectContactMessage :: ( MsgEncodingI e , ChatMonad m ) => Contact -> ChatMsgEvent e -> m ( SndMessage , Int64 )
2023-01-07 19:47:51 +04:00
sendDirectContactMessage ct @ Contact { activeConn = conn @ Connection { connId , connStatus } } chatMsgEvent
| connStatus /= ConnReady && connStatus /= ConnSndReady = throwChatError $ CEContactNotReady ct
| connDisabled conn = throwChatError $ CEContactDisabled ct
| otherwise = sendDirectMessage conn chatMsgEvent ( ConnectionId connId )
2022-02-14 18:49:42 +04:00
2022-10-14 13:06:33 +01:00
sendDirectMessage :: ( MsgEncodingI e , ChatMonad m ) => Connection -> ChatMsgEvent e -> ConnOrGroupId -> m ( SndMessage , Int64 )
2022-02-25 21:59:35 +04:00
sendDirectMessage conn chatMsgEvent connOrGroupId = do
2023-01-07 19:47:51 +04:00
when ( connDisabled conn ) $ throwChatError ( CEConnectionDisabled conn )
2022-03-13 19:34:03 +00:00
msg @ SndMessage { msgId , msgBody } <- createSndMessage chatMsgEvent connOrGroupId
2022-10-14 13:06:33 +01:00
( msg , ) <$> deliverMessage conn ( toCMEventTag chatMsgEvent ) msgBody msgId
2022-01-24 16:07:17 +00:00
2022-10-14 13:06:33 +01:00
createSndMessage :: ( MsgEncodingI e , ChatMonad m ) => ChatMsgEvent e -> ConnOrGroupId -> m SndMessage
2022-02-25 21:59:35 +04:00
createSndMessage chatMsgEvent connOrGroupId = do
2022-03-13 19:34:03 +00:00
gVar <- asks idsDrg
2023-09-01 19:20:07 +04:00
ChatConfig { chatVRange } <- asks config
2022-06-18 20:06:13 +01:00
withStore $ \ db -> createNewSndMessage db gVar connOrGroupId $ \ sharedMsgId ->
2023-09-01 19:20:07 +04:00
let msgBody = strEncode ChatMessage { chatVRange , msgId = Just sharedMsgId , chatMsgEvent }
2022-03-16 13:20:47 +00:00
in NewMessage { chatMsgEvent , msgBody }
2021-07-16 07:40:55 +01:00
2023-09-01 19:20:07 +04:00
directMessage :: ( MsgEncodingI e , ChatMonad m ) => ChatMsgEvent e -> m ByteString
directMessage chatMsgEvent = do
ChatConfig { chatVRange } <- asks config
pure $ strEncode ChatMessage { chatVRange , msgId = Nothing , chatMsgEvent }
2021-07-16 07:40:55 +01:00
2022-10-14 13:06:33 +01:00
deliverMessage :: ChatMonad m => Connection -> CMEventTag e -> MsgBody -> MessageId -> m Int64
2022-06-07 14:14:54 +01:00
deliverMessage conn @ Connection { connId } cmEventTag msgBody msgId = do
let msgFlags = MsgFlags { notification = hasNotification cmEventTag }
agentMsgId <- withAgent $ \ a -> sendMessage a ( aConnId conn ) msgFlags msgBody
2021-12-29 23:11:55 +04:00
let sndMsgDelivery = SndMsgDelivery { connId , agentMsgId }
2023-01-06 14:22:16 +04:00
withStoreCtx'
( Just $ " createSndMsgDelivery, sndMsgDelivery: " <> show sndMsgDelivery <> " , msgId: " <> show msgId <> " , cmEventTag: " <> show cmEventTag <> " , msgDeliveryStatus: MDSSndAgent " )
$ \ db -> createSndMsgDelivery db sndMsgDelivery msgId
2021-12-29 23:11:55 +04:00
2023-07-26 14:49:35 +04:00
sendGroupMessage :: ( MsgEncodingI e , ChatMonad m ) => User -> GroupInfo -> [ GroupMember ] -> ChatMsgEvent e -> m ( SndMessage , [ GroupMember ] )
2023-01-13 14:19:21 +04:00
sendGroupMessage user GroupInfo { groupId } members chatMsgEvent =
sendGroupMessage' user members chatMsgEvent groupId Nothing $ pure ()
2022-01-24 16:07:17 +00:00
2023-07-26 14:49:35 +04:00
sendGroupMessage' :: forall e m . ( MsgEncodingI e , ChatMonad m ) => User -> [ GroupMember ] -> ChatMsgEvent e -> Int64 -> Maybe Int64 -> m () -> m ( SndMessage , [ GroupMember ] )
2023-01-13 14:19:21 +04:00
sendGroupMessage' user members chatMsgEvent groupId introId_ postDeliver = do
2023-01-12 16:31:27 +04:00
msg <- createSndMessage chatMsgEvent ( GroupId groupId )
2022-02-10 17:03:36 +04:00
-- TODO collect failed deliveries into a single error
2023-07-26 14:49:35 +04:00
rs <- forM ( filter memberCurrent members ) $ \ m ->
messageMember m msg ` catchChatError ` ( \ e -> toView ( CRChatError ( Just user ) e ) $> Nothing )
let sentToMembers = catMaybes rs
pure ( msg , sentToMembers )
2023-01-12 16:31:27 +04:00
where
2023-07-26 14:49:35 +04:00
messageMember :: GroupMember -> SndMessage -> m ( Maybe GroupMember )
2023-01-12 16:31:27 +04:00
messageMember m @ GroupMember { groupMemberId } SndMessage { msgId , msgBody } = case memberConn m of
2023-07-26 14:49:35 +04:00
Nothing -> do
withStore' $ \ db -> createPendingGroupMessage db groupMemberId msgId introId_
pure $ Just m
2022-06-07 14:14:54 +01:00
Just conn @ Connection { connStatus }
2023-07-26 14:49:35 +04:00
| connDisabled conn || connStatus == ConnDeleted -> pure Nothing
2022-06-07 14:14:54 +01:00
| connStatus == ConnSndReady || connStatus == ConnReady -> do
2022-10-15 14:48:07 +04:00
let tag = toCMEventTag chatMsgEvent
2023-01-12 16:31:27 +04:00
deliverMessage conn tag msgBody msgId >> postDeliver
2023-07-26 14:49:35 +04:00
pure $ Just m
| otherwise -> do
withStore' $ \ db -> createPendingGroupMessage db groupMemberId msgId introId_
pure $ Just m
2022-01-24 16:07:17 +00:00
2023-01-13 14:19:21 +04:00
sendPendingGroupMessages :: ChatMonad m => User -> GroupMember -> Connection -> m ()
sendPendingGroupMessages user GroupMember { groupMemberId , localDisplayName } conn = do
2022-06-18 20:06:13 +01:00
pendingMessages <- withStore' $ \ db -> getPendingGroupMessages db groupMemberId
2022-01-24 16:07:17 +00:00
-- TODO ensure order - pending messages interleave with user input messages
2023-01-12 16:31:27 +04:00
forM_ pendingMessages $ \ pgm ->
2023-07-09 23:24:38 +01:00
processPendingMessage pgm ` catchChatError ` ( toView . CRChatError ( Just user ) )
2023-01-12 16:31:27 +04:00
where
processPendingMessage PendingGroupMessage { msgId , cmEventTag = ACMEventTag _ tag , msgBody , introId_ } = do
void $ deliverMessage conn tag msgBody msgId
withStore' $ \ db -> deletePendingGroupMessage db groupMemberId msgId
case tag of
XGrpMemFwd_ -> case introId_ of
Just introId -> withStore' $ \ db -> updateIntroStatus db introId GMIntroInvForwarded
_ -> throwChatError $ CEGroupMemberIntroNotFound localDisplayName
_ -> pure ()
2022-01-24 16:07:17 +00:00
2023-09-01 19:20:07 +04:00
saveRcvMSG :: ChatMonad m => Connection -> ConnOrGroupId -> MsgMeta -> MsgBody -> CommandId -> m ( Connection , RcvMessage )
2023-05-26 17:36:06 +04:00
saveRcvMSG conn @ Connection { connId } connOrGroupId agentMsgMeta msgBody agentAckCmdId = do
2023-09-01 19:20:07 +04:00
ACMsg _ ChatMessage { chatVRange , msgId = sharedMsgId_ , chatMsgEvent } <- parseAChatMessage conn agentMsgMeta msgBody
2023-09-06 11:41:23 +04:00
conn' <- updatePeerChatVRange conn chatVRange
2022-01-24 16:07:17 +00:00
let agentMsgId = fst $ recipient agentMsgMeta
2022-03-16 13:20:47 +00:00
newMsg = NewMessage { chatMsgEvent , msgBody }
2022-09-14 19:45:21 +04:00
rcvMsgDelivery = RcvMsgDelivery { connId , agentMsgId , agentMsgMeta , agentAckCmdId }
2023-09-01 19:20:07 +04:00
msg <- withStoreCtx'
2023-01-06 14:22:16 +04:00
( Just $ " createNewMessageAndRcvMsgDelivery, rcvMsgDelivery: " <> show rcvMsgDelivery <> " , sharedMsgId_: " <> show sharedMsgId_ <> " , msgDeliveryStatus: MDSRcvAgent " )
$ \ db -> createNewMessageAndRcvMsgDelivery db connOrGroupId newMsg sharedMsgId_ rcvMsgDelivery
2023-09-01 19:20:07 +04:00
pure ( conn' , msg )
2022-01-26 16:18:27 +04:00
2022-12-16 07:51:04 +00:00
saveSndChatItem :: ChatMonad m => User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> m ( ChatItem c 'MDSnd )
saveSndChatItem user cd msg content = saveSndChatItem' user cd msg content Nothing Nothing Nothing False
2022-12-15 15:17:29 +04:00
2022-12-16 07:51:04 +00:00
saveSndChatItem' :: ChatMonad m => User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> Maybe ( CIFile 'MDSnd ) -> Maybe ( CIQuote c ) -> Maybe CITimed -> Bool -> m ( ChatItem c 'MDSnd )
2022-12-17 13:58:16 +00:00
saveSndChatItem' user cd msg @ SndMessage { sharedMsgId } content ciFile quotedItem itemTimed live = do
2022-01-28 11:52:10 +04:00
createdAt <- liftIO getCurrentTime
2022-12-22 17:43:10 +04:00
ciId <- withStore' $ \ db -> do
when ( ciRequiresAttention content ) $ updateChatTs db user cd createdAt
ciId <- createNewSndChatItem db user cd msg content quotedItem itemTimed live createdAt
forM_ ciFile $ \ CIFile { fileId } -> updateFileTransferChatItemId db fileId ciId createdAt
pure ciId
2022-12-17 13:58:16 +00:00
liftIO $ mkChatItem cd ciId content ciFile quotedItem ( Just sharedMsgId ) itemTimed live createdAt createdAt
2022-01-26 16:18:27 +04:00
2022-12-16 07:51:04 +00:00
saveRcvChatItem :: ChatMonad m => User -> ChatDirection c 'MDRcv -> RcvMessage -> MsgMeta -> CIContent 'MDRcv -> m ( ChatItem c 'MDRcv )
saveRcvChatItem user cd msg @ RcvMessage { sharedMsgId_ } msgMeta content =
saveRcvChatItem' user cd msg sharedMsgId_ msgMeta content Nothing Nothing False
2022-12-15 15:17:29 +04:00
2022-12-16 07:51:04 +00:00
saveRcvChatItem' :: ChatMonad m => User -> ChatDirection c 'MDRcv -> RcvMessage -> Maybe SharedMsgId -> MsgMeta -> CIContent 'MDRcv -> Maybe ( CIFile 'MDRcv ) -> Maybe CITimed -> Bool -> m ( ChatItem c 'MDRcv )
2022-12-17 13:58:16 +00:00
saveRcvChatItem' user cd msg sharedMsgId_ MsgMeta { broker = ( _ , brokerTs ) } content ciFile itemTimed live = do
2022-01-28 11:52:10 +04:00
createdAt <- liftIO getCurrentTime
2022-12-22 17:43:10 +04:00
( ciId , quotedItem ) <- withStore' $ \ db -> do
when ( ciRequiresAttention content ) $ updateChatTs db user cd createdAt
( ciId , quotedItem ) <- createNewRcvChatItem db user cd msg sharedMsgId_ content itemTimed live brokerTs createdAt
forM_ ciFile $ \ CIFile { fileId } -> updateFileTransferChatItemId db fileId ciId createdAt
pure ( ciId , quotedItem )
2022-12-17 13:58:16 +00:00
liftIO $ mkChatItem cd ciId content ciFile quotedItem sharedMsgId_ itemTimed live brokerTs createdAt
2022-03-16 13:20:47 +00:00
2022-12-22 17:43:10 +04:00
mkChatItem :: forall c d . MsgDirectionI d => ChatDirection c d -> ChatItemId -> CIContent d -> Maybe ( CIFile d ) -> Maybe ( CIQuote c ) -> Maybe SharedMsgId -> Maybe CITimed -> Bool -> ChatItemTs -> UTCTime -> IO ( ChatItem c d )
2022-12-17 13:58:16 +00:00
mkChatItem cd ciId content file quotedItem sharedMsgId itemTimed live itemTs currentTs = do
2022-03-16 13:20:47 +00:00
let itemText = ciContentToText content
2022-11-28 16:27:22 +04:00
itemStatus = ciCreateStatus content
2023-06-08 11:07:21 +04:00
meta = mkCIMeta ciId content itemText itemStatus sharedMsgId Nothing False itemTimed ( justTrue live ) currentTs itemTs currentTs currentTs
2023-05-15 12:28:53 +02:00
pure ChatItem { chatDir = toCIDirection cd , meta , content , formattedText = parseMaybeMarkdownList itemText , quotedItem , reactions = [] , file }
2021-07-24 10:26:28 +01:00
2022-12-15 15:17:29 +04:00
deleteDirectCI :: ChatMonad m => User -> Contact -> CChatItem 'CTDirect -> Bool -> Bool -> m ChatResponse
deleteDirectCI user ct ci @ ( CChatItem msgDir deletedItem @ ChatItem { file } ) byUser timed = do
2022-11-30 19:42:33 +04:00
deleteCIFile user file
2023-05-29 15:18:22 +04:00
withStoreCtx' ( Just " deleteDirectCI, deleteDirectChatItem " ) $ \ db -> deleteDirectChatItem db user ct ci
2023-01-04 21:06:28 +04:00
pure $ CRChatItemDeleted user ( AChatItem SCTDirect msgDir ( DirectChat ct ) deletedItem ) Nothing byUser timed
2022-11-30 19:42:33 +04:00
2023-05-19 14:52:51 +02:00
deleteGroupCI :: ChatMonad m => User -> GroupInfo -> CChatItem 'CTGroup -> Bool -> Bool -> Maybe GroupMember -> UTCTime -> m ChatResponse
deleteGroupCI user gInfo ci @ ( CChatItem msgDir deletedItem @ ChatItem { file } ) byUser timed byGroupMember_ deletedTs = do
2022-11-30 19:42:33 +04:00
deleteCIFile user file
2023-05-29 15:18:22 +04:00
toCi <- withStoreCtx' ( Just " deleteGroupCI, deleteGroupChatItem ... " ) $ \ db ->
2023-02-08 07:08:53 +00:00
case byGroupMember_ of
Nothing -> deleteGroupChatItem db user gInfo ci $> Nothing
2023-05-19 14:52:51 +02:00
Just m -> Just <$> updateGroupChatItemModerated db user gInfo ci m deletedTs
2023-02-08 07:08:53 +00:00
pure $ CRChatItemDeleted user ( AChatItem SCTGroup msgDir ( GroupChat gInfo ) deletedItem ) toCi byUser timed
2022-11-30 19:42:33 +04:00
deleteCIFile :: ( ChatMonad m , MsgDirectionI d ) => User -> Maybe ( CIFile d ) -> m ()
2023-09-01 19:43:27 +01:00
deleteCIFile user file_ =
forM_ file_ $ \ file -> do
fileAgentConnIds <- deleteFile' user ( mkCIFileInfo file ) True
2023-01-24 16:24:34 +04:00
deleteAgentConnectionsAsync user fileAgentConnIds
2022-11-30 19:42:33 +04:00
2023-05-19 14:52:51 +02:00
markDirectCIDeleted :: ChatMonad m => User -> Contact -> CChatItem 'CTDirect -> MessageId -> Bool -> UTCTime -> m ChatResponse
markDirectCIDeleted user ct @ Contact { contactId } ci @ ( CChatItem _ ChatItem { file } ) msgId byUser deletedTs = do
2023-04-03 18:49:22 +04:00
cancelCIFile user file
toCi <- withStore $ \ db -> do
2023-05-19 14:52:51 +02:00
liftIO $ markDirectChatItemDeleted db user ct ci msgId deletedTs
2023-04-03 18:49:22 +04:00
getDirectChatItem db user contactId ( cchatItemId ci )
pure $ CRChatItemDeleted user ( ctItem ci ) ( Just $ ctItem toCi ) byUser False
where
ctItem ( CChatItem msgDir ci' ) = AChatItem SCTDirect msgDir ( DirectChat ct ) ci'
2022-11-30 19:42:33 +04:00
2023-05-19 14:52:51 +02:00
markGroupCIDeleted :: ChatMonad m => User -> GroupInfo -> CChatItem 'CTGroup -> MessageId -> Bool -> Maybe GroupMember -> UTCTime -> m ChatResponse
markGroupCIDeleted user gInfo @ GroupInfo { groupId } ci @ ( CChatItem _ ChatItem { file } ) msgId byUser byGroupMember_ deletedTs = do
2023-04-03 18:49:22 +04:00
cancelCIFile user file
toCi <- withStore $ \ db -> do
2023-05-19 14:52:51 +02:00
liftIO $ markGroupChatItemDeleted db user gInfo ci msgId byGroupMember_ deletedTs
2023-04-03 18:49:22 +04:00
getGroupChatItem db user groupId ( cchatItemId ci )
pure $ CRChatItemDeleted user ( gItem ci ) ( Just $ gItem toCi ) byUser False
where
gItem ( CChatItem msgDir ci' ) = AChatItem SCTGroup msgDir ( GroupChat gInfo ) ci'
cancelCIFile :: ( ChatMonad m , MsgDirectionI d ) => User -> Maybe ( CIFile d ) -> m ()
2023-09-01 19:43:27 +01:00
cancelCIFile user file_ =
forM_ file_ $ \ file -> do
fileAgentConnIds <- cancelFile' user ( mkCIFileInfo file ) True
2023-04-03 18:49:22 +04:00
deleteAgentConnectionsAsync user fileAgentConnIds
2022-11-30 19:42:33 +04:00
2023-09-10 22:40:15 +03:00
createAgentConnectionAsync :: forall m c . ( ChatMonad m , ConnectionModeI c ) => User -> CommandFunction -> Bool -> SConnectionMode c -> SubscriptionMode -> m ( CommandId , ConnId )
createAgentConnectionAsync user cmdFunction enableNtfs cMode subMode = do
2022-10-15 14:48:07 +04:00
cmdId <- withStore' $ \ db -> createCommand db user Nothing cmdFunction
2023-09-10 22:40:15 +03:00
connId <- withAgent $ \ a -> createConnectionAsync a ( aUserId user ) ( aCorrId cmdId ) enableNtfs cMode subMode
2022-09-14 19:45:21 +04:00
pure ( cmdId , connId )
2023-09-10 22:40:15 +03:00
joinAgentConnectionAsync :: ChatMonad m => User -> Bool -> ConnectionRequestUri c -> ConnInfo -> SubscriptionMode -> m ( CommandId , ConnId )
joinAgentConnectionAsync user enableNtfs cReqUri cInfo subMode = do
2022-09-14 19:45:21 +04:00
cmdId <- withStore' $ \ db -> createCommand db user Nothing CFJoinConn
2023-09-10 22:40:15 +03:00
connId <- withAgent $ \ a -> joinConnectionAsync a ( aUserId user ) ( aCorrId cmdId ) enableNtfs cReqUri cInfo subMode
2022-09-14 19:45:21 +04:00
pure ( cmdId , connId )
2022-10-14 13:06:33 +01:00
allowAgentConnectionAsync :: ( MsgEncodingI e , ChatMonad m ) => User -> Connection -> ConfirmationId -> ChatMsgEvent e -> m ()
2022-09-14 19:45:21 +04:00
allowAgentConnectionAsync user conn @ Connection { connId } confId msg = do
cmdId <- withStore' $ \ db -> createCommand db user ( Just connId ) CFAllowConn
2023-09-01 19:20:07 +04:00
dm <- directMessage msg
withAgent $ \ a -> allowConnectionAsync a ( aCorrId cmdId ) ( aConnId conn ) confId dm
2022-06-18 20:06:13 +01:00
withStore' $ \ db -> updateConnectionStatus db conn ConnAccepted
2021-07-05 19:54:44 +01:00
2023-09-10 22:40:15 +03:00
agentAcceptContactAsync :: ( MsgEncodingI e , ChatMonad m ) => User -> Bool -> InvitationId -> ChatMsgEvent e -> SubscriptionMode -> m ( CommandId , ConnId )
agentAcceptContactAsync user enableNtfs invId msg subMode = do
2022-10-14 14:57:01 +04:00
cmdId <- withStore' $ \ db -> createCommand db user Nothing CFAcceptContact
2023-09-01 19:20:07 +04:00
dm <- directMessage msg
2023-09-10 22:40:15 +03:00
connId <- withAgent $ \ a -> acceptContactAsync a ( aCorrId cmdId ) enableNtfs invId dm subMode
2022-10-14 14:57:01 +04:00
pure ( cmdId , connId )
2023-01-24 16:24:34 +04:00
deleteAgentConnectionAsync :: ChatMonad m => User -> ConnId -> m ()
deleteAgentConnectionAsync user acId =
2023-07-09 23:24:38 +01:00
withAgent ( ` deleteConnectionAsync ` acId ) ` catchChatError ` ( toView . CRChatError ( Just user ) )
2022-09-30 16:18:43 +04:00
2023-01-24 16:24:34 +04:00
deleteAgentConnectionsAsync :: ChatMonad m => User -> [ ConnId ] -> m ()
deleteAgentConnectionsAsync _ [] = pure ()
deleteAgentConnectionsAsync user acIds =
2023-07-09 23:24:38 +01:00
withAgent ( ` deleteConnectionsAsync ` acIds ) ` catchChatError ` ( toView . CRChatError ( Just user ) )
2022-09-30 16:18:43 +04:00
2023-04-25 15:46:00 +04:00
agentXFTPDeleteRcvFile :: ChatMonad m => RcvFileId -> FileTransferId -> m ()
agentXFTPDeleteRcvFile aFileId fileId = do
withAgent ( ` xftpDeleteRcvFile ` aFileId )
2023-03-21 15:21:14 +04:00
withStore' $ \ db -> setRcvFTAgentDeleted db fileId
2023-04-14 15:32:12 +04:00
agentXFTPDeleteSndFileRemote :: ChatMonad m => User -> XFTPSndFile -> FileTransferId -> m ()
agentXFTPDeleteSndFileRemote user XFTPSndFile { agentSndFileId = AgentSndFileId aFileId , privateSndFileDescr , agentSndFileDeleted } fileId =
unless agentSndFileDeleted $
forM_ privateSndFileDescr $ \ sfdText -> do
sd <- parseFileDescription sfdText
withAgent $ \ a -> xftpDeleteSndFileRemote a ( aUserId user ) aFileId sd
withStore' $ \ db -> setSndFTAgentDeleted db user fileId
2022-11-01 17:32:49 +03:00
userProfileToSend :: User -> Maybe Profile -> Maybe Contact -> Profile
2022-11-04 17:05:21 +00:00
userProfileToSend user @ User { profile = p } incognitoProfile ct =
let p' = fromMaybe ( fromLocalProfile p ) incognitoProfile
userPrefs = maybe ( preferences' user ) ( const Nothing ) incognitoProfile
in ( p' :: Profile ) { preferences = Just . toChatPrefs $ mergePreferences ( userPreferences <$> ct ) userPrefs }
2022-11-01 17:32:49 +03:00
2022-12-22 14:56:29 +00:00
createRcvFeatureItems :: forall m . ChatMonad m => User -> Contact -> Contact -> m ()
createRcvFeatureItems user ct ct' =
createFeatureItems user ct ct' CDDirectRcv CIRcvChatFeature CIRcvChatPreference contactPreference
createSndFeatureItems :: forall m . ChatMonad m => User -> Contact -> Contact -> m ()
createSndFeatureItems user ct ct' =
createFeatureItems user ct ct' CDDirectSnd CISndChatFeature CISndChatPreference getPref
where
2023-08-25 04:56:37 +08:00
getPref u = ( userPreference u ) . preference
2022-12-22 14:56:29 +00:00
type FeatureContent a d = ChatFeature -> a -> Maybe Int -> CIContent d
createFeatureItems ::
forall d m .
( MsgDirectionI d , ChatMonad m ) =>
User ->
Contact ->
Contact ->
( Contact -> ChatDirection 'CTDirect d ) ->
FeatureContent PrefEnabled d ->
FeatureContent FeatureAllowed d ->
( forall f . ContactUserPreference ( FeaturePreference f ) -> FeaturePreference f ) ->
m ()
createFeatureItems user Contact { mergedPreferences = cups } ct' @ Contact { mergedPreferences = cups' } chatDir ciFeature ciOffer getPref =
forM_ allChatFeatures $ \ ( ACF f ) -> createItem f
where
createItem :: forall f . FeatureI f => SChatFeature f -> m ()
createItem f
| state /= state' = create ciFeature state'
| prefState /= prefState' = create ciOffer prefState'
| otherwise = pure ()
where
create :: FeatureContent a d -> ( a , Maybe Int ) -> m ()
create ci ( s , param ) = createInternalChatItem user ( chatDir ct' ) ( ci f' s param ) Nothing
f' = chatFeature f
state = featureState cup
state' = featureState cup'
prefState = preferenceState $ getPref cup
prefState' = preferenceState $ getPref cup'
cup = getContactUserPreference f cups
cup' = getContactUserPreference f cups'
2022-11-23 11:04:08 +00:00
2022-12-19 21:18:59 +04:00
createGroupFeatureChangedItems :: ( MsgDirectionI d , ChatMonad m ) => User -> ChatDirection 'CTGroup d -> ( GroupFeature -> GroupPreference -> Maybe Int -> CIContent d ) -> GroupInfo -> GroupInfo -> m ()
createGroupFeatureChangedItems user cd ciContent GroupInfo { fullGroupPreferences = gps } GroupInfo { fullGroupPreferences = gps' } =
2022-12-14 08:30:24 +00:00
forM_ allGroupFeatures $ \ ( AGF f ) -> do
2022-12-19 21:18:59 +04:00
let state = groupFeatureState $ getGroupPreference f gps
pref' = getGroupPreference f gps'
state' @ ( _ , int' ) = groupFeatureState pref'
when ( state /= state' ) $
createInternalChatItem user cd ( ciContent ( toGroupFeature f ) ( toGroupPreference pref' ) int' ) Nothing
2022-11-23 11:04:08 +00:00
sameGroupProfileInfo :: GroupProfile -> GroupProfile -> Bool
sameGroupProfileInfo p p' = p { groupPreferences = Nothing } == p' { groupPreferences = Nothing }
createInternalChatItem :: forall c d m . ( ChatTypeI c , MsgDirectionI d , ChatMonad m ) => User -> ChatDirection c d -> CIContent d -> Maybe UTCTime -> m ()
createInternalChatItem user cd content itemTs_ = do
createdAt <- liftIO getCurrentTime
let itemTs = fromMaybe createdAt itemTs_
2022-12-22 17:43:10 +04:00
ciId <- withStore' $ \ db -> do
when ( ciRequiresAttention content ) $ updateChatTs db user cd createdAt
createNewChatItemNoMsg db user cd content itemTs createdAt
2022-12-16 07:51:04 +00:00
ci <- liftIO $ mkChatItem cd ciId content Nothing Nothing Nothing Nothing False itemTs createdAt
2023-01-04 21:06:28 +04:00
toView $ CRNewChatItem user ( AChatItem ( chatTypeI @ c ) ( msgDirection @ d ) ( toChatInfo cd ) ci )
2022-11-23 11:04:08 +00:00
2023-08-07 08:25:15 +01:00
getCreateActiveUser :: SQLiteStore -> Bool -> IO User
getCreateActiveUser st testView = do
2021-07-05 19:54:44 +01:00
user <-
2022-06-18 20:06:13 +01:00
withTransaction st getUsers >>= \ case
2021-07-05 19:54:44 +01:00
[] -> newUser
users -> maybe ( selectUser users ) pure ( find activeUser users )
2023-08-07 08:25:15 +01:00
unless testView $ putStrLn $ " Current user: " <> userStr user
2021-07-05 19:54:44 +01:00
pure user
where
newUser :: IO User
newUser = do
putStrLn
" No user profiles found, it will be created now. \ n \
2021-07-14 20:11:41 +01:00
\ Please choose your display name and your full name .\ n \
2021-07-05 19:54:44 +01:00
\ They will be sent to your contacts when you connect .\ n \
\ They are only stored on your device and you can change them later . "
loop
where
loop = do
2021-07-14 20:11:41 +01:00
displayName <- getContactName
fullName <- T . pack <$> getWithPrompt " full name (optional) "
2023-04-27 17:19:21 +04:00
withTransaction st ( \ db -> runExceptT $ createUserRecord db ( AgentUserId 1 ) Profile { displayName , fullName , image = Nothing , contactLink = Nothing , preferences = Nothing } True ) >>= \ case
2021-07-14 20:11:41 +01:00
Left SEDuplicateName -> do
putStrLn " chosen display name is already used by another profile on this device, choose another one "
2021-07-05 19:54:44 +01:00
loop
Left e -> putStrLn ( " database error " <> show e ) >> exitFailure
Right user -> pure user
selectUser :: [ User ] -> IO User
selectUser [ user ] = do
2023-08-25 04:56:37 +08:00
withTransaction st ( ` setActiveUser ` user . userId )
2021-07-05 19:54:44 +01:00
pure user
selectUser users = do
2021-07-05 20:05:07 +01:00
putStrLn " Select user profile: "
2021-07-05 19:54:44 +01:00
forM_ ( zip [ 1 .. ] users ) $ \ ( n :: Int , user ) -> putStrLn $ show n <> " - " <> userStr user
loop
where
loop = do
nStr <- getWithPrompt $ " user profile number (1 .. " <> show ( length users ) <> " ) "
case readMaybe nStr :: Maybe Int of
Nothing -> putStrLn " invalid user number " >> loop
Just n
| n <= 0 || n > length users -> putStrLn " invalid user number " >> loop
| otherwise -> do
2022-10-15 14:48:07 +04:00
let user = users !! ( n - 1 )
2023-08-25 04:56:37 +08:00
withTransaction st ( ` setActiveUser ` user . userId )
2022-10-15 14:48:07 +04:00
pure user
2021-07-05 19:54:44 +01:00
userStr :: User -> String
2022-08-18 11:35:31 +04:00
userStr User { localDisplayName , profile = LocalProfile { fullName } } =
2021-08-22 15:56:36 +01:00
T . unpack $ localDisplayName <> if T . null fullName || localDisplayName == fullName then " " else " ( " <> fullName <> " ) "
2021-07-14 20:11:41 +01:00
getContactName :: IO ContactName
getContactName = do
displayName <- getWithPrompt " display name (no spaces) "
if null displayName || isJust ( find ( == ' ' ) displayName )
then putStrLn " display name has space(s), choose another one " >> getContactName
else pure $ T . pack displayName
2021-07-05 19:54:44 +01:00
getWithPrompt :: String -> IO String
getWithPrompt s = putStr ( s <> " : " ) >> hFlush stdout >> getLine
2021-06-25 18:18:24 +01:00
2023-03-22 15:58:01 +00:00
whenUserNtfs :: ChatMonad' m => User -> m () -> m ()
whenUserNtfs User { showNtfs , activeUser } = when $ showNtfs || activeUser
whenContactNtfs :: ChatMonad' m => User -> Contact -> m () -> m ()
whenContactNtfs user Contact { chatSettings } = whenUserNtfs user . when ( enableNtfs chatSettings )
whenGroupNtfs :: ChatMonad' m => User -> GroupInfo -> m () -> m ()
whenGroupNtfs user GroupInfo { chatSettings } = whenUserNtfs user . when ( enableNtfs chatSettings )
showMsgToast :: ChatMonad' m => Text -> MsgContent -> Maybe MarkdownList -> m ()
2022-03-13 20:13:47 +00:00
showMsgToast from mc md_ = showToast from $ maybe ( msgContentText mc ) ( mconcat . map hideSecret ) md_
where
hideSecret :: FormattedText -> Text
hideSecret FormattedText { format = Just Secret } = " ... "
hideSecret FormattedText { text } = text
2023-03-22 15:58:01 +00:00
showToast :: ChatMonad' m => Text -> Text -> m ()
2021-07-04 18:42:24 +01:00
showToast title text = atomically . ( ` writeTBQueue ` Notification { title , text } ) =<< asks notifyQ
2023-03-22 15:58:01 +00:00
notificationSubscriber :: ChatMonad' m => m ()
2021-06-26 20:20:33 +01:00
notificationSubscriber = do
ChatController { notifyQ , sendNotification } <- ask
forever $ atomically ( readTBQueue notifyQ ) >>= liftIO . sendNotification
2023-01-04 21:06:28 +04:00
withUser' :: ChatMonad m => ( User -> m ChatResponse ) -> m ChatResponse
2022-02-06 16:18:01 +00:00
withUser' action =
asks currentUser
>>= readTVarIO
2023-01-16 17:25:06 +00:00
>>= maybe ( throwChatError CENoActiveUser ) run
where
2023-07-09 23:24:38 +01:00
run u = action u ` catchChatError ` ( pure . CRChatCmdError ( Just u ) )
2022-02-06 16:18:01 +00:00
2023-01-04 21:06:28 +04:00
withUser :: ChatMonad m => ( User -> m ChatResponse ) -> m ChatResponse
2022-02-06 16:18:01 +00:00
withUser action = withUser' $ \ user ->
ifM chatStarted ( action user ) ( throwChatError CEChatNotStarted )
2022-10-03 17:44:56 +04:00
2023-01-13 12:24:54 +00:00
withUserId :: ChatMonad m => UserId -> ( User -> m ChatResponse ) -> m ChatResponse
withUserId userId action = withUser $ \ user -> do
checkSameUser userId user
action user
checkSameUser :: ChatMonad m => UserId -> User -> m ()
checkSameUser userId User { userId = activeUserId } = when ( userId /= activeUserId ) $ throwChatError ( CEDifferentActiveUser userId activeUserId )
2022-10-03 17:44:56 +04:00
chatStarted :: ChatMonad m => m Bool
chatStarted = fmap isJust . readTVarIO =<< asks agentAsync
2022-02-06 16:18:01 +00:00
2022-12-22 19:18:38 +04:00
waitChatStarted :: ChatMonad m => m ()
waitChatStarted = do
agentStarted <- asks agentAsync
atomically $ readTVar agentStarted >>= \ a -> unless ( isJust a ) retry
2021-07-05 19:54:44 +01:00
withAgent :: ChatMonad m => ( AgentClient -> ExceptT AgentErrorType m a ) -> m a
withAgent action =
2021-06-25 18:18:24 +01:00
asks smpAgent
>>= runExceptT . action
2023-09-01 19:43:27 +01:00
>>= liftEither . first ( ` ChatErrorAgent ` Nothing )
2021-06-25 18:18:24 +01:00
2022-06-18 20:06:13 +01:00
withStore' :: ChatMonad m => ( DB . Connection -> IO a ) -> m a
withStore' action = withStore $ liftIO . action
2023-01-06 14:22:16 +04:00
withStore :: ChatMonad m => ( DB . Connection -> ExceptT StoreError IO a ) -> m a
withStore = withStoreCtx Nothing
withStoreCtx' :: ChatMonad m => Maybe String -> ( DB . Connection -> IO a ) -> m a
withStoreCtx' ctx_ action = withStoreCtx ctx_ $ liftIO . action
withStoreCtx :: ChatMonad m => Maybe String -> ( DB . Connection -> ExceptT StoreError IO a ) -> m a
withStoreCtx ctx_ action = do
2022-09-23 19:22:56 +01:00
ChatController { chatStore } <- ask
2023-05-29 15:18:22 +04:00
liftEitherError ChatErrorStore $ case ctx_ of
Nothing -> withTransaction chatStore ( runExceptT . action ) ` E . catch ` handleInternal " "
-- uncomment to debug store performance
-- Just ctx -> do
-- t1 <- liftIO getCurrentTime
-- putStrLn $ "withStoreCtx start :: " <> show t1 <> " :: " <> ctx
-- r <- withTransactionCtx ctx_ chatStore (runExceptT . action) `E.catch` handleInternal (" (" <> ctx <> ")")
-- t2 <- liftIO getCurrentTime
-- putStrLn $ "withStoreCtx end :: " <> show t2 <> " :: " <> ctx <> " :: duration=" <> show (diffToMilliseconds $ diffUTCTime t2 t1)
-- pure r
2023-06-12 13:45:39 +04:00
Just _ -> withTransaction chatStore ( runExceptT . action ) ` E . catch ` handleInternal " "
2022-06-18 20:06:13 +01:00
where
2023-05-29 15:18:22 +04:00
handleInternal :: String -> E . SomeException -> IO ( Either StoreError a )
handleInternal ctxStr e = pure . Left . SEInternalError $ show e <> ctxStr
2021-07-04 18:42:24 +01:00
2021-06-25 18:18:24 +01:00
chatCommandP :: Parser ChatCommand
chatCommandP =
2022-11-09 10:48:24 +00:00
choice
2023-07-13 23:48:25 +01:00
[ " /mute " *> ( ( ` SetShowMessages ` False ) <$> chatNameP ) ,
" /unmute " *> ( ( ` SetShowMessages ` True ) <$> chatNameP ) ,
" /receipts " *> ( SetSendReceipts <$> chatNameP <* " " <*> ( ( Just <$> onOffP ) <|> ( " default " $> Nothing ) ) ) ,
2023-05-09 10:33:30 +02:00
" /_create user " *> ( CreateActiveUser <$> jsonP ) ,
" /create user " *> ( CreateActiveUser <$> newUserP ) ,
2023-01-04 21:06:28 +04:00
" /users " $> ListUsers ,
2023-03-22 15:58:01 +00:00
" /_user " *> ( APISetActiveUser <$> A . decimal <*> optional ( A . space *> jsonP ) ) ,
( " /user " <|> " /u " ) *> ( SetActiveUser <$> displayName <*> optional ( A . space *> pwdP ) ) ,
2023-07-13 23:48:25 +01:00
" /set receipts all " *> ( SetAllContactReceipts <$> onOffP ) ,
2023-07-26 14:49:35 +04:00
" /_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 ) ,
2023-03-22 15:58:01 +00:00
" /_hide user " *> ( APIHideUser <$> A . decimal <* A . space <*> jsonP ) ,
2023-03-29 19:28:06 +01:00
" /_unhide user " *> ( APIUnhideUser <$> A . decimal <* A . space <*> jsonP ) ,
" /_mute user " *> ( APIMuteUser <$> A . decimal ) ,
" /_unmute user " *> ( APIUnmuteUser <$> A . decimal ) ,
2023-03-22 15:58:01 +00:00
" /hide user " *> ( HideUser <$> pwdP ) ,
2023-03-29 19:28:06 +01:00
" /unhide user " *> ( UnhideUser <$> pwdP ) ,
2023-03-22 15:58:01 +00:00
" /mute user " $> MuteUser ,
" /unmute user " $> UnmuteUser ,
" /_delete user " *> ( APIDeleteUser <$> A . decimal <* " del_smp= " <*> onOffP <*> optional ( A . space *> jsonP ) ) ,
" /delete user " *> ( DeleteUser <$> displayName <*> pure True <*> optional ( A . space *> pwdP ) ) ,
2022-07-20 09:36:43 +01:00
( " /user " <|> " /u " ) $> ShowActiveUser ,
2023-04-18 19:43:16 +04:00
" /_start subscribe= " *> ( StartChat <$> onOffP <* " expire= " <*> onOffP <* " xftp= " <*> onOffP ) ,
" /_start " $> StartChat True True True ,
2022-07-20 09:36:43 +01:00
" /_stop " $> APIStopChat ,
" /_app activate " $> APIActivateChat ,
" /_app suspend " *> ( APISuspendChat <$> A . decimal ) ,
" /_resubscribe all " $> ResubscribeAllConnections ,
2023-03-22 18:48:38 +04:00
" /_temp_folder " *> ( SetTempFolder <$> filePath ) ,
2023-03-19 11:49:30 +00:00
( " /_files_folder " <|> " /files_folder " ) *> ( SetFilesFolder <$> filePath ) ,
2023-03-22 22:20:12 +04:00
" /_xftp " *> ( APISetXFTPConfig <$> ( " on " *> ( Just <$> jsonP ) <|> ( " off " $> Nothing ) ) ) ,
2023-04-04 14:58:26 +01:00
" /xftp " *> ( APISetXFTPConfig <$> ( " on " *> ( Just <$> xftpCfgP ) <|> ( " off " $> Nothing ) ) ) ,
2022-07-20 09:36:43 +01:00
" /_db export " *> ( APIExportArchive <$> jsonP ) ,
2023-03-19 11:49:30 +00:00
" /db export " $> ExportArchive ,
2022-07-20 09:36:43 +01:00
" /_db import " *> ( APIImportArchive <$> jsonP ) ,
" /_db delete " $> APIDeleteStorage ,
2022-09-06 21:25:07 +01:00
" /_db encryption " *> ( APIStorageEncryption <$> jsonP ) ,
2022-09-05 14:54:39 +01:00
" /db encrypt " *> ( APIStorageEncryption . DBEncryptionConfig " " <$> dbKeyP ) ,
2022-09-08 17:36:16 +01:00
" /db key " *> ( APIStorageEncryption <$> ( DBEncryptionConfig <$> dbKeyP <* A . space <*> dbKeyP ) ) ,
2022-09-05 14:54:39 +01:00
" /db decrypt " *> ( APIStorageEncryption . ( ` DBEncryptionConfig ` " " ) <$> dbKeyP ) ,
2022-09-17 16:06:27 +01:00
" /sql chat " *> ( ExecChatStoreSQL <$> textP ) ,
" /sql agent " *> ( ExecAgentStoreSQL <$> textP ) ,
2023-08-12 18:27:10 +01:00
" /sql slow " $> SlowSQLQueries ,
2023-01-05 20:38:31 +04:00
" /_get chats " *> ( APIGetChats <$> A . decimal <*> ( " pcc=on " $> True <|> " pcc=off " $> False <|> pure False ) ) ,
2022-11-14 08:42:54 +00:00
" /_get chat " *> ( APIGetChat <$> chatRefP <* A . space <*> chatPaginationP <*> optional ( " search= " *> stringP ) ) ,
2023-04-27 09:12:34 +02:00
" /_get items " *> ( APIGetChatItems <$> chatPaginationP <*> optional ( " search= " *> stringP ) ) ,
2023-05-18 17:52:58 +02:00
" /_get item info " *> ( APIGetChatItemInfo <$> chatRefP <* A . space <*> A . decimal ) ,
2023-05-11 16:00:01 +04:00
" /_send " *> ( APISendMessage <$> chatRefP <*> liveMessageP <*> sendMessageTTLP <*> ( " json " *> jsonP <|> " text " *> ( ComposedMessage Nothing Nothing <$> mcTextP ) ) ) ,
2022-12-16 07:51:04 +00:00
" /_update item " *> ( APIUpdateChatItem <$> chatRefP <* A . space <*> A . decimal <*> liveMessageP <* A . space <*> msgContentP ) ,
2022-07-20 09:36:43 +01:00
" /_delete item " *> ( APIDeleteChatItem <$> chatRefP <* A . space <*> A . decimal <* A . space <*> ciDeleteMode ) ,
2023-02-08 07:08:53 +00:00
" /_delete member item # " *> ( APIDeleteMemberChatItem <$> A . decimal <* A . space <*> A . decimal <* A . space <*> A . decimal ) ,
2023-05-17 01:22:00 +02:00
" /_reaction " *> ( APIChatItemReaction <$> chatRefP <* A . space <*> A . decimal <* A . space <*> onOffP <* A . space <*> jsonP ) ,
2022-07-20 09:36:43 +01:00
" /_read chat " *> ( APIChatRead <$> chatRefP <*> optional ( A . space *> ( ( , ) <$> ( " from= " *> A . decimal ) <* A . space <*> ( " to= " *> A . decimal ) ) ) ) ,
2022-10-19 21:38:44 +03:00
" /_unread chat " *> ( APIChatUnread <$> chatRefP <* A . space <*> onOffP ) ,
2022-07-20 09:36:43 +01:00
" /_delete " *> ( APIDeleteChat <$> chatRefP ) ,
" /_clear chat " *> ( APIClearChat <$> chatRefP ) ,
2023-08-08 17:25:28 +04:00
" /_accept " *> ( APIAcceptContact <$> incognitoOnOffP <* A . space <*> A . decimal ) ,
2022-07-20 09:36:43 +01:00
" /_reject " *> ( APIRejectContact <$> A . decimal ) ,
" /_call invite @ " *> ( APISendCallInvitation <$> A . decimal <* A . space <*> jsonP ) ,
2022-12-09 21:50:01 +00:00
" /call " *> char_ '@' *> ( SendCallInvitation <$> displayName <*> pure defaultCallType ) ,
2022-07-20 09:36:43 +01:00
" /_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 ) ,
2023-01-16 15:06:03 +04:00
" /_call get " $> APIGetCallInvitations ,
2023-01-05 20:38:31 +04:00
" /_profile " *> ( APIUpdateProfile <$> A . decimal <* A . space <*> jsonP ) ,
2022-08-24 19:03:43 +04:00
" /_set alias @ " *> ( APISetContactAlias <$> A . decimal <*> ( A . space *> textP <|> pure " " ) ) ,
2022-09-27 20:45:46 +01:00
" /_set alias : " *> ( APISetConnectionAlias <$> A . decimal <*> ( A . space *> textP <|> pure " " ) ) ,
2022-11-01 17:32:49 +03:00
" /_set prefs @ " *> ( APISetContactPrefs <$> A . decimal <* A . space <*> jsonP ) ,
2022-07-20 09:36:43 +01:00
" /_parse " *> ( APIParseMarkdown . safeDecodeUtf8 <$> A . takeByteString ) ,
" /_ntf get " $> APIGetNtfToken ,
" /_ntf register " *> ( APIRegisterToken <$> strP_ <*> strP ) ,
" /_ntf verify " *> ( APIVerifyToken <$> strP <* A . space <*> strP <* A . space <*> strP ) ,
" /_ntf delete " *> ( APIDeleteToken <$> strP ) ,
2023-01-20 10:48:25 +00:00
" /_ntf message " *> ( APIGetNtfMessage <$> strP <* A . space <*> strP ) ,
2022-07-20 09:36:43 +01:00
" /_add # " *> ( APIAddMember <$> A . decimal <* A . space <*> A . decimal <*> memberRole ) ,
" /_join # " *> ( APIJoinGroup <$> A . decimal ) ,
2022-10-03 09:00:47 +01:00
" /_member role # " *> ( APIMemberRole <$> A . decimal <* A . space <*> A . decimal <*> memberRole ) ,
2022-07-20 09:36:43 +01:00
" /_remove # " *> ( APIRemoveMember <$> A . decimal <* A . space <*> A . decimal ) ,
" /_leave # " *> ( APILeaveGroup <$> A . decimal ) ,
" /_members # " *> ( APIListMembers <$> A . decimal ) ,
2023-04-05 21:59:12 +01:00
" /_server test " *> ( APITestProtoServer <$> A . decimal <* A . space <*> strP ) ,
" /smp test " *> ( TestProtoServer . AProtoServerWithAuth SPSMP <$> strP ) ,
" /xftp test " *> ( TestProtoServer . AProtoServerWithAuth SPXFTP <$> strP ) ,
" /_servers " *> ( APISetUserProtoServers <$> A . decimal <* A . space <*> srvCfgP ) ,
" /smp " *> ( SetUserProtoServers . APSC SPSMP . ProtoServersConfig . map toServerCfg <$> protocolServersP ) ,
" /smp default " $> SetUserProtoServers ( APSC SPSMP $ ProtoServersConfig [] ) ,
" /xftp " *> ( SetUserProtoServers . APSC SPXFTP . ProtoServersConfig . map toServerCfg <$> protocolServersP ) ,
" /xftp default " $> SetUserProtoServers ( APSC SPXFTP $ ProtoServersConfig [] ) ,
" /_servers " *> ( APIGetUserProtoServers <$> A . decimal <* A . space <*> strP ) ,
" /smp " $> GetUserProtoServers ( AProtocolType SPSMP ) ,
" /xftp " $> GetUserProtoServers ( AProtocolType SPXFTP ) ,
2023-01-05 20:38:31 +04:00
" /_ttl " *> ( APISetChatItemTTL <$> A . decimal <* A . space <*> ciTTLDecimal ) ,
" /ttl " *> ( SetChatItemTTL <$> ciTTL ) ,
" /_ttl " *> ( APIGetChatItemTTL <$> A . decimal ) ,
" /ttl " $> GetChatItemTTL ,
2022-07-25 14:04:27 +01:00
" /_network " *> ( APISetNetworkConfig <$> jsonP ) ,
( " /network " <|> " /net " ) *> ( APISetNetworkConfig <$> netCfgP ) ,
( " /network " <|> " /net " ) $> APIGetNetworkConfig ,
2023-07-05 09:09:56 +01:00
" /reconnect " $> ReconnectAllServers ,
2022-08-19 22:44:00 +01:00
" /_settings " *> ( APISetChatSettings <$> chatRefP <* A . space <*> jsonP ) ,
2022-07-20 14:57:16 +01:00
" /_info # " *> ( APIGroupMemberInfo <$> A . decimal <* A . space <*> A . decimal ) ,
2023-08-06 11:56:40 +01:00
" /_info # " *> ( APIGroupInfo <$> A . decimal ) ,
2022-07-20 14:57:16 +01:00
" /_info @ " *> ( APIContactInfo <$> A . decimal ) ,
2022-12-09 21:50:01 +00:00
( " /info # " <|> " /i # " ) *> ( GroupMemberInfo <$> displayName <* A . space <* char_ '@' <*> displayName ) ,
2023-08-06 11:56:40 +01:00
( " /info # " <|> " /i # " ) *> ( ShowGroupInfo <$> displayName ) ,
2022-12-09 21:50:01 +00:00
( " /info " <|> " /i " ) *> char_ '@' *> ( ContactInfo <$> displayName ) ,
2022-11-01 13:26:08 +00:00
" /_switch # " *> ( APISwitchGroupMember <$> A . decimal <* A . space <*> A . decimal ) ,
" /_switch @ " *> ( APISwitchContact <$> A . decimal ) ,
2023-06-16 19:05:53 +04:00
" /_abort switch # " *> ( APIAbortSwitchGroupMember <$> A . decimal <* A . space <*> A . decimal ) ,
" /_abort switch @ " *> ( APIAbortSwitchContact <$> A . decimal ) ,
2023-07-05 19:44:21 +04:00
" /_sync # " *> ( APISyncGroupMemberRatchet <$> A . decimal <* A . space <*> A . decimal <*> ( " force=on " $> True <|> pure False ) ) ,
" /_sync @ " *> ( APISyncContactRatchet <$> A . decimal <*> ( " force=on " $> True <|> pure False ) ) ,
2022-12-09 21:50:01 +00:00
" /switch # " *> ( SwitchGroupMember <$> displayName <* A . space <* char_ '@' <*> displayName ) ,
" /switch " *> char_ '@' *> ( SwitchContact <$> displayName ) ,
2023-06-16 19:05:53 +04:00
" /abort switch # " *> ( AbortSwitchGroupMember <$> displayName <* A . space <* char_ '@' <*> displayName ) ,
" /abort switch " *> char_ '@' *> ( AbortSwitchContact <$> displayName ) ,
2023-07-05 19:44:21 +04:00
" /sync # " *> ( SyncGroupMemberRatchet <$> displayName <* A . space <* char_ '@' <*> displayName <*> ( " force=on " $> True <|> pure False ) ) ,
" /sync " *> char_ '@' *> ( SyncContactRatchet <$> displayName <*> ( " force=on " $> True <|> pure False ) ) ,
2022-12-09 15:26:43 +00:00
" /_get code @ " *> ( APIGetContactCode <$> A . decimal ) ,
" /_get code # " *> ( APIGetGroupMemberCode <$> A . decimal <* A . space <*> A . decimal ) ,
2022-12-10 12:09:45 +00:00
" /_verify code @ " *> ( APIVerifyContact <$> A . decimal <*> optional ( A . space *> textP ) ) ,
" /_verify code # " *> ( APIVerifyGroupMember <$> A . decimal <* A . space <*> A . decimal <*> optional ( A . space *> textP ) ) ,
2023-01-07 19:47:51 +04:00
" /_enable @ " *> ( APIEnableContact <$> A . decimal ) ,
" /_enable # " *> ( APIEnableGroupMember <$> A . decimal <* A . space <*> A . decimal ) ,
2022-12-09 21:50:01 +00:00
" /code " *> char_ '@' *> ( GetContactCode <$> displayName ) ,
" /code # " *> ( GetGroupMemberCode <$> displayName <* A . space <* char_ '@' <*> displayName ) ,
2022-12-10 12:09:45 +00:00
" /verify " *> char_ '@' *> ( VerifyContact <$> displayName <*> optional ( A . space *> textP ) ) ,
" /verify # " *> ( VerifyGroupMember <$> displayName <* A . space <* char_ '@' <*> displayName <*> optional ( A . space *> textP ) ) ,
2023-01-07 19:47:51 +04:00
" /enable " *> char_ '@' *> ( EnableContact <$> displayName ) ,
" /enable # " *> ( EnableGroupMember <$> displayName <* A . space <* char_ '@' <*> displayName ) ,
2022-07-20 09:36:43 +01:00
( " /help files " <|> " /help file " <|> " /hf " ) $> ChatHelp HSFiles ,
( " /help groups " <|> " /help group " <|> " /hg " ) $> ChatHelp HSGroups ,
2023-03-04 22:33:17 +00:00
( " /help contacts " <|> " /help contact " <|> " /hc " ) $> ChatHelp HSContacts ,
2022-07-20 09:36:43 +01:00
( " /help address " <|> " /ha " ) $> ChatHelp HSMyAddress ,
2023-08-08 17:25:28 +04:00
" /help incognito " $> ChatHelp HSIncognito ,
2022-07-20 09:36:43 +01:00
( " /help messages " <|> " /hm " ) $> ChatHelp HSMessages ,
2022-07-26 07:29:28 +01:00
( " /help settings " <|> " /hs " ) $> ChatHelp HSSettings ,
2023-03-19 11:49:30 +00:00
( " /help db " <|> " /hd " ) $> ChatHelp HSDatabase ,
2022-07-20 09:36:43 +01:00
( " /help " <|> " /h " ) $> ChatHelp HSMain ,
2022-12-09 21:50:01 +00:00
( " /group " <|> " /g " ) *> char_ '#' *> ( NewGroup <$> groupProfile ) ,
2023-01-05 20:38:31 +04:00
" /_group " *> ( APINewGroup <$> A . decimal <* A . space <*> jsonP ) ,
2023-08-14 07:37:04 +01:00
( " /add " <|> " /a " ) *> char_ '#' *> ( AddMember <$> displayName <* A . space <* char_ '@' <*> displayName <*> ( memberRole <|> pure GRMember ) ) ,
2022-12-09 21:50:01 +00:00
( " /join " <|> " /j " ) *> char_ '#' *> ( JoinGroup <$> displayName ) ,
( " /member role " <|> " /mr " ) *> char_ '#' *> ( MemberRole <$> displayName <* A . space <* char_ '@' <*> displayName <*> memberRole ) ,
( " /remove " <|> " /rm " ) *> char_ '#' *> ( RemoveMember <$> displayName <* A . space <* char_ '@' <*> displayName ) ,
( " /leave " <|> " /l " ) *> char_ '#' *> ( LeaveGroup <$> displayName ) ,
2022-07-20 09:36:43 +01:00
( " /delete # " <|> " /d # " ) *> ( DeleteGroup <$> displayName ) ,
2022-12-09 21:50:01 +00:00
( " /delete " <|> " /d " ) *> char_ '@' *> ( DeleteContact <$> displayName ) ,
2022-07-20 09:36:43 +01:00
" /clear # " *> ( ClearGroup <$> displayName ) ,
2022-12-09 21:50:01 +00:00
" /clear " *> char_ '@' *> ( ClearContact <$> displayName ) ,
( " /members " <|> " /ms " ) *> char_ '#' *> ( ListMembers <$> displayName ) ,
2023-08-01 20:54:51 +01:00
" /_groups " *> ( APIListGroups <$> A . decimal <*> optional ( " @ " *> A . decimal ) <*> optional ( A . space *> stringP ) ) ,
( " /groups " <|> " /gs " ) *> ( ListGroups <$> optional ( " @ " *> displayName ) <*> optional ( A . space *> stringP ) ) ,
2022-07-29 19:04:32 +01:00
" /_group_profile # " *> ( APIUpdateGroupProfile <$> A . decimal <* A . space <*> jsonP ) ,
2022-12-10 08:27:32 +00:00
( " /group_profile " <|> " /gp " ) *> char_ '#' *> ( UpdateGroupNames <$> displayName <* A . space <*> groupProfile ) ,
( " /group_profile " <|> " /gp " ) *> char_ '#' *> ( ShowGroupProfile <$> displayName ) ,
2023-02-18 15:16:50 +00:00
" /group_descr " *> char_ '#' *> ( UpdateGroupDescription <$> displayName <*> optional ( A . space *> msgTextP ) ) ,
2023-08-01 20:54:51 +01:00
" /set welcome " *> char_ '#' *> ( UpdateGroupDescription <$> displayName <* A . space <*> ( Just <$> msgTextP ) ) ,
" /delete welcome " *> char_ '#' *> ( UpdateGroupDescription <$> displayName <*> pure Nothing ) ,
" /show welcome " *> char_ '#' *> ( ShowGroupDescription <$> displayName ) ,
2023-03-06 09:51:42 +00:00
" /_create link # " *> ( APICreateGroupLink <$> A . decimal <*> ( memberRole <|> pure GRMember ) ) ,
" /_set link role # " *> ( APIGroupLinkMemberRole <$> A . decimal <*> memberRole ) ,
2022-10-13 17:12:22 +04:00
" /_delete link # " *> ( APIDeleteGroupLink <$> A . decimal ) ,
" /_get link # " *> ( APIGetGroupLink <$> A . decimal ) ,
2023-03-06 09:51:42 +00:00
" /create link # " *> ( CreateGroupLink <$> displayName <*> ( memberRole <|> pure GRMember ) ) ,
" /set link role # " *> ( GroupLinkMemberRole <$> displayName <*> memberRole ) ,
2022-10-13 17:12:22 +04:00
" /delete link # " *> ( DeleteGroupLink <$> displayName ) ,
" /show link # " *> ( ShowGroupLink <$> displayName ) ,
2023-09-16 17:55:48 +04:00
" /_create member contact # " *> ( APICreateMemberContact <$> A . decimal <* A . space <*> A . decimal ) ,
" /_invite member contact @ " *> ( APISendMemberContactInvitation <$> A . decimal <*> optional ( A . space *> msgContentP ) ) ,
2023-02-18 15:16:50 +00:00
( " ># " <|> " > # " ) *> ( SendGroupMessageQuote <$> displayName <* A . space <*> pure Nothing <*> quotedMsg <*> msgTextP ) ,
( " ># " <|> " > # " ) *> ( SendGroupMessageQuote <$> displayName <* A . space <* char_ '@' <*> ( Just <$> displayName ) <* A . space <*> quotedMsg <*> msgTextP ) ,
2023-01-05 20:38:31 +04:00
" /_contacts " *> ( APIListContacts <$> A . decimal ) ,
2023-01-16 12:10:47 +00:00
" /contacts " $> ListContacts ,
2023-08-08 17:25:28 +04:00
" /_connect " *> ( APIConnect <$> A . decimal <*> incognitoOnOffP <* A . space <*> ( ( Just <$> strP ) <|> A . takeByteString $> Nothing ) ) ,
" /_connect " *> ( APIAddContact <$> A . decimal <*> incognitoOnOffP ) ,
" /_set incognito : " *> ( APISetConnectionIncognito <$> A . decimal <* A . space <*> onOffP ) ,
( " /connect " <|> " /c " ) *> ( Connect <$> incognitoP <* A . space <*> ( ( Just <$> strP ) <|> A . takeByteString $> Nothing ) ) ,
( " /connect " <|> " /c " ) *> ( AddContact <$> incognitoP ) ,
2023-02-18 15:16:50 +00:00
SendMessage <$> chatNameP <* A . space <*> msgTextP ,
2023-09-16 21:30:20 +04:00
" @# " *> ( SendMemberContactMessage <$> displayName <* A . space <* char_ '@' <*> displayName <* A . space <*> msgTextP ) ,
2023-02-18 15:16:50 +00:00
" /live " *> ( SendLiveMessage <$> chatNameP <*> ( A . space *> msgTextP <|> pure " " ) ) ,
2022-07-20 09:36:43 +01:00
( " >@ " <|> " > @ " ) *> sendMsgQuote ( AMsgDirection SMDRcv ) ,
( " >>@ " <|> " >> @ " ) *> sendMsgQuote ( AMsgDirection SMDSnd ) ,
2023-02-18 15:16:50 +00:00
( " \ \ " <|> " \ \ " ) *> ( DeleteMessage <$> chatNameP <* A . space <*> textP ) ,
( " \ \ \ \ # " <|> " \ \ \ \ # " ) *> ( DeleteMemberMessage <$> displayName <* A . space <* char_ '@' <*> displayName <* A . space <*> textP ) ,
( " ! " <|> " ! " ) *> ( EditMessage <$> chatNameP <* A . space <*> ( quotedMsg <|> pure " " ) <*> msgTextP ) ,
2023-05-17 01:22:00 +02:00
ReactToMessage <$> ( ( " + " $> True ) <|> ( " - " $> False ) ) <*> reactionP <* A . space <*> chatNameP' <* A . space <*> textP ,
2023-02-18 15:16:50 +00:00
" /feed " *> ( SendMessageBroadcast <$> msgTextP ) ,
2023-01-16 12:10:47 +00:00
( " /chats " <|> " /cs " ) *> ( LastChats <$> ( " all " $> Nothing <|> Just <$> ( A . space *> A . decimal <|> pure 20 ) ) ) ,
2022-11-14 08:42:54 +00:00
( " /tail " <|> " /t " ) *> ( LastMessages <$> optional ( A . space *> chatNameP ) <*> msgCountP <*> pure Nothing ) ,
( " /search " <|> " /? " ) *> ( LastMessages <$> optional ( A . space *> chatNameP ) <*> msgCountP <*> ( Just <$> ( A . space *> stringP ) ) ) ,
2022-12-17 15:33:58 +00:00
" /last_item_id " *> ( LastChatItemId <$> optional ( A . space *> chatNameP ) <*> ( A . space *> A . decimal <|> pure 0 ) ) ,
2022-12-19 11:16:50 +00:00
" /show " *> ( ShowLiveItems <$> ( A . space *> onOffP <|> pure True ) ) ,
" /show " *> ( ShowChatItem . Just <$> A . decimal ) ,
2023-05-08 20:07:51 +04:00
" /item info " *> ( ShowChatItemInfo <$> chatNameP <* A . space <*> msgTextP ) ,
2022-07-20 09:36:43 +01:00
( " /file " <|> " /f " ) *> ( SendFile <$> chatNameP' <* A . space <*> filePath ) ,
( " /image " <|> " /img " ) *> ( SendImage <$> chatNameP' <* A . space <*> filePath ) ,
( " /fforward " <|> " /ff " ) *> ( ForwardFile <$> chatNameP' <* A . space <*> A . decimal ) ,
( " /image_forward " <|> " /imgf " ) *> ( ForwardImage <$> chatNameP' <* A . space <*> A . decimal ) ,
2023-03-09 11:01:22 +00:00
( " /fdescription " <|> " /fd " ) *> ( SendFileDescription <$> chatNameP' <* A . space <*> filePath ) ,
2023-09-01 19:43:27 +01:00
( " /freceive " <|> " /fr " ) *> ( ReceiveFile <$> A . decimal <*> ( " encrypt= " *> onOffP <|> pure False ) <*> optional ( " inline= " *> onOffP ) <*> optional ( A . space *> filePath ) ) ,
" /_set_file_to_receive " *> ( SetFileToReceive <$> A . decimal <*> ( " encrypt= " *> onOffP <|> pure False ) ) ,
2022-07-20 09:36:43 +01:00
( " /fcancel " <|> " /fc " ) *> ( CancelFile <$> A . decimal ) ,
( " /fstatus " <|> " /fs " ) *> ( FileStatus <$> A . decimal ) ,
2023-08-08 17:25:28 +04:00
" /simplex " *> ( ConnectSimplex <$> incognitoP ) ,
2023-01-05 20:38:31 +04:00
" /_address " *> ( APICreateMyAddress <$> A . decimal ) ,
2022-07-20 09:36:43 +01:00
( " /address " <|> " /ad " ) $> CreateMyAddress ,
2023-01-05 20:38:31 +04:00
" /_delete_address " *> ( APIDeleteMyAddress <$> A . decimal ) ,
2022-07-20 09:36:43 +01:00
( " /delete_address " <|> " /da " ) $> DeleteMyAddress ,
2023-01-05 20:38:31 +04:00
" /_show_address " *> ( APIShowMyAddress <$> A . decimal ) ,
2022-07-20 09:36:43 +01:00
( " /show_address " <|> " /sa " ) $> ShowMyAddress ,
2023-04-27 17:19:21 +04:00
" /_profile_address " *> ( APISetProfileAddress <$> A . decimal <* A . space <*> onOffP ) ,
( " /profile_address " <|> " /pa " ) *> ( SetProfileAddress <$> onOffP ) ,
2023-01-05 20:38:31 +04:00
" /_auto_accept " *> ( APIAddressAutoAccept <$> A . decimal <* A . space <*> autoAcceptP ) ,
2022-10-21 19:14:12 +03:00
" /auto_accept " *> ( AddressAutoAccept <$> autoAcceptP ) ,
2023-08-08 17:25:28 +04:00
( " /accept " <|> " /ac " ) *> ( AcceptContact <$> incognitoP <* A . space <* char_ '@' <*> displayName ) ,
2022-12-09 21:50:01 +00:00
( " /reject " <|> " /rc " ) *> char_ '@' *> ( RejectContact <$> displayName ) ,
2022-07-20 09:36:43 +01:00
( " /markdown " <|> " /m " ) $> ChatHelp HSMarkdown ,
( " /welcome " <|> " /w " ) $> Welcome ,
2023-06-17 10:34:04 +01:00
" /set profile image " *> ( UpdateProfileImage . Just . ImageData <$> imageP ) ,
" /delete profile image " $> UpdateProfileImage Nothing ,
" /show profile image " $> ShowProfileImage ,
2023-05-09 10:33:30 +02:00
( " /profile " <|> " /p " ) *> ( uncurry UpdateProfile <$> profileNames ) ,
2022-07-20 09:36:43 +01:00
( " /profile " <|> " /p " ) $> ShowProfile ,
2022-12-14 08:30:24 +00:00
" /set voice # " *> ( SetGroupFeature ( AGF SGFVoice ) <$> displayName <*> ( A . space *> strP ) ) ,
" /set voice @ " *> ( SetContactFeature ( ACF SCFVoice ) <$> displayName <*> optional ( A . space *> strP ) ) ,
" /set voice " *> ( SetUserFeature ( ACF SCFVoice ) <$> strP ) ,
2023-06-24 12:36:07 +01:00
" /set files # " *> ( SetGroupFeature ( AGF SGFFiles ) <$> displayName <*> ( A . space *> strP ) ) ,
2023-04-17 11:18:04 +02:00
" /set calls @ " *> ( SetContactFeature ( ACF SCFCalls ) <$> displayName <*> optional ( A . space *> strP ) ) ,
" /set calls " *> ( SetUserFeature ( ACF SCFCalls ) <$> strP ) ,
2022-12-14 08:30:24 +00:00
" /set delete # " *> ( SetGroupFeature ( AGF SGFFullDelete ) <$> displayName <*> ( A . space *> strP ) ) ,
" /set delete @ " *> ( SetContactFeature ( ACF SCFFullDelete ) <$> displayName <*> optional ( A . space *> strP ) ) ,
" /set delete " *> ( SetUserFeature ( ACF SCFFullDelete ) <$> strP ) ,
" /set direct # " *> ( SetGroupFeature ( AGF SGFDirectMessages ) <$> displayName <*> ( A . space *> strP ) ) ,
2022-12-21 19:54:44 +04:00
" /set disappear # " *> ( SetGroupTimedMessages <$> displayName <*> ( A . space *> timedTTLOnOffP ) ) ,
2022-12-17 14:49:03 +04:00
" /set disappear @ " *> ( SetContactTimedMessages <$> displayName <*> optional ( A . space *> timedMessagesEnabledP ) ) ,
2022-12-21 19:54:44 +04:00
" /set disappear " *> ( SetUserTimedMessages <$> ( ( " yes " $> True ) <|> ( " no " $> False ) ) ) ,
2023-08-08 17:25:28 +04:00
( " /incognito " <* optional ( A . space *> onOffP ) ) $> ChatHelp HSIncognito ,
2022-07-20 09:36:43 +01:00
( " /quit " <|> " /q " <|> " /exit " ) $> QuitChat ,
2022-10-22 21:22:44 +01:00
( " /version " <|> " /v " ) $> ShowVersion ,
2022-12-26 22:24:34 +00:00
" /debug locks " $> DebugLocks ,
" /get stats " $> GetAgentStats ,
2023-08-25 14:10:40 +01:00
" /reset stats " $> ResetAgentStats ,
" /get subs " $> GetAgentSubs ,
" /get subs details " $> GetAgentSubsDetails
2022-07-20 09:36:43 +01:00
]
2021-06-25 18:18:24 +01:00
where
2022-11-09 10:48:24 +00:00
choice = A . choice . map ( \ p -> p <* A . takeWhile ( == ' ' ) <* A . endOfInput )
2023-08-08 17:25:28 +04:00
incognitoP = ( A . space *> ( " incognito " <|> " i " ) ) $> True <|> pure False
incognitoOnOffP = ( A . space *> " incognito= " *> onOffP ) <|> pure False
2022-03-10 15:45:40 +04:00
imagePrefix = ( <> ) <$> " data: " <*> ( " image/png;base64, " <|> " image/jpg;base64, " )
imageP = safeDecodeUtf8 <$> ( ( <> ) <$> imagePrefix <*> ( B64 . encode <$> base64P ) )
2022-04-23 17:32:40 +01:00
chatTypeP = A . char '@' $> CTDirect <|> A . char '#' $> CTGroup <|> A . char ':' $> CTContactConnection
2022-02-01 15:05:27 +04:00
chatPaginationP =
( CPLast <$ " count= " <*> A . decimal )
<|> ( CPAfter <$ " after= " <*> A . decimal <* A . space <* " count= " <*> A . decimal )
<|> ( CPBefore <$ " before= " <*> A . decimal <* A . space <* " count= " <*> A . decimal )
2022-05-06 09:17:49 +01:00
mcTextP = MCText . safeDecodeUtf8 <$> A . takeByteString
msgContentP = " text " *> mcTextP <|> " json " *> jsonP
2022-03-28 20:35:57 +04:00
ciDeleteMode = " broadcast " $> CIDMBroadcast <|> " internal " $> CIDMInternal
2021-07-14 20:11:41 +01:00
displayName = safeDecodeUtf8 <$> ( B . cons <$> A . satisfy refChar <*> A . takeTill ( == ' ' ) )
2023-02-18 15:16:50 +00:00
sendMsgQuote msgDir = SendMessageQuote <$> displayName <* A . space <*> pure msgDir <*> quotedMsg <*> msgTextP
quotedMsg = safeDecodeUtf8 <$> ( A . char '(' *> A . takeTill ( == ')' ) <* A . char ')' ) <* optional A . space
2023-05-15 12:28:53 +02:00
reactionP = MREmoji <$> ( mrEmojiChar <$?> ( toEmoji <$> A . anyChar ) )
toEmoji = \ case
'1' -> '👍'
'+' -> '👍'
'-' -> '👎'
')' -> '😀'
2023-05-20 14:42:50 +02:00
',' -> '😢'
2023-05-15 12:28:53 +02:00
'*' -> head " ❤️ "
'^' -> '🚀'
c -> c
2021-07-11 12:22:22 +01:00
refChar c = c > ' ' && c /= '#' && c /= '@'
2022-12-16 07:51:04 +00:00
liveMessageP = " live= " *> onOffP <|> pure False
2023-05-11 16:00:01 +04:00
sendMessageTTLP = " ttl= " *> ( ( Just <$> A . decimal ) <|> ( " default " $> Nothing ) ) <|> pure Nothing
2023-07-13 23:48:25 +01:00
receiptSettings = do
enable <- onOffP
clearOverrides <- ( " clear_overrides= " *> onOffP ) <|> pure False
pure UserMsgReceiptSettings { enable , clearOverrides }
2022-02-14 14:59:11 +04:00
onOffP = ( " on " $> True ) <|> ( " off " $> False )
2023-05-09 10:33:30 +02:00
profileNames = ( , ) <$> displayName <*> fullNameP
newUserP = do
2023-06-06 14:17:14 +04:00
sameServers <- " same_servers= " *> onOffP <* A . space <|> pure False
2023-05-09 10:33:30 +02:00
( cName , fullName ) <- profileNames
let profile = Just Profile { displayName = cName , fullName , image = Nothing , contactLink = Nothing , preferences = Nothing }
pure NewUser { profile , sameServers , pastTimestamp = False }
2022-03-23 20:52:00 +00:00
jsonP :: J . FromJSON a => Parser a
jsonP = J . eitherDecodeStrict' <$?> A . takeByteString
2021-07-12 19:00:03 +01:00
groupProfile = do
2023-05-09 10:33:30 +02:00
( gName , fullName ) <- profileNames
2022-12-14 08:30:24 +00:00
let groupPreferences = Just ( emptyGroupPrefs :: GroupPreferences ) { directMessages = Just DirectMessagesGroupPreference { enable = FEOn } }
2022-12-10 08:27:32 +00:00
pure GroupProfile { displayName = gName , fullName , description = Nothing , image = Nothing , groupPreferences }
2023-05-09 10:33:30 +02:00
fullNameP = A . space *> textP <|> pure " "
2022-08-24 19:03:43 +04:00
textP = safeDecodeUtf8 <$> A . takeByteString
2023-03-22 15:58:01 +00:00
pwdP = jsonP <|> ( UserPwd . safeDecodeUtf8 <$> A . takeTill ( == ' ' ) )
2023-02-18 15:16:50 +00:00
msgTextP = jsonP <|> textP
2022-11-14 08:42:54 +00:00
stringP = T . unpack . safeDecodeUtf8 <$> A . takeByteString
filePath = stringP
2021-07-11 12:22:22 +01:00
memberRole =
2022-10-01 20:30:47 +01:00
A . choice
[ " owner " $> GROwner ,
" admin " $> GRAdmin ,
" member " $> GRMember ,
2023-03-06 09:51:42 +00:00
" observer " $> GRObserver
2022-10-01 20:30:47 +01:00
]
2022-04-28 08:34:21 +01:00
chatNameP = ChatName <$> chatTypeP <*> displayName
2022-04-30 19:18:46 +04:00
chatNameP' = ChatName <$> ( chatTypeP <|> pure CTDirect ) <*> displayName
2022-04-28 08:34:21 +01:00
chatRefP = ChatRef <$> chatTypeP <*> A . decimal
2022-04-28 07:26:43 +01:00
msgCountP = A . space *> A . decimal <|> pure 10
2022-09-28 20:47:06 +04:00
ciTTLDecimal = ( " none " $> Nothing ) <|> ( Just <$> A . decimal )
ciTTL =
( " day " $> Just 86400 )
<|> ( " week " $> Just ( 7 * 86400 ) )
<|> ( " month " $> Just ( 30 * 86400 ) )
<|> ( " none " $> Nothing )
2022-12-17 14:49:03 +04:00
timedTTLP =
( " 30s " $> 30 )
<|> ( " 5min " $> 300 )
<|> ( " 1h " $> 3600 )
<|> ( " 8h " $> ( 8 * 3600 ) )
<|> ( " day " $> 86400 )
<|> ( " week " $> ( 7 * 86400 ) )
<|> ( " month " $> ( 30 * 86400 ) )
2022-12-21 19:54:44 +04:00
timedTTLOnOffP =
optional ( " on " *> A . space ) *> ( Just <$> timedTTLP )
<|> ( " off " $> Nothing )
2022-12-17 14:49:03 +04:00
timedMessagesEnabledP =
2022-12-21 19:54:44 +04:00
optional ( " yes " *> A . space ) *> ( TMEEnableSetTTL <$> timedTTLP )
2022-12-17 14:49:03 +04:00
<|> ( " yes " $> TMEEnableKeepTTL )
<|> ( " no " $> TMEDisableKeepTTL )
2022-07-25 14:04:27 +01:00
netCfgP = do
socksProxy <- " socks= " *> ( " off " $> Nothing <|> " on " $> Just defaultSocksProxy <|> Just <$> strP )
t_ <- optional $ " timeout= " *> A . decimal
2022-12-27 12:05:13 +00:00
logErrors <- " log= " *> onOffP <|> pure False
2022-07-25 14:04:27 +01:00
let tcpTimeout = 1000000 * fromMaybe ( maybe 5 ( const 10 ) socksProxy ) t_
2022-12-27 12:05:13 +00:00
pure $ fullNetworkConfig socksProxy tcpTimeout logErrors
2023-04-04 14:58:26 +01:00
xftpCfgP = XFTPFileConfig <$> ( " size= " *> fileSizeP <|> pure 0 )
2023-03-22 22:20:12 +04:00
fileSizeP =
A . choice
[ gb <$> A . decimal <* " gb " ,
mb <$> A . decimal <* " mb " ,
kb <$> A . decimal <* " kb " ,
A . decimal
]
2022-09-05 14:54:39 +01:00
dbKeyP = nonEmptyKey <$?> strP
nonEmptyKey k @ ( DBEncryptionKey s ) = if null s then Left " empty key " else Right k
2022-10-21 19:14:12 +03:00
autoAcceptP =
ifM
onOffP
( Just <$> ( AutoAccept <$> ( " incognito= " *> onOffP <|> pure False ) <*> optional ( A . space *> msgContentP ) ) )
( pure Nothing )
2023-04-05 21:59:12 +01:00
srvCfgP = strP >>= \ case AProtocolType p -> APSC p <$> ( A . space *> jsonP )
2022-11-21 07:43:41 +00:00
toServerCfg server = ServerCfg { server , preset = False , tested = Nothing , enabled = True }
2022-12-09 21:50:01 +00:00
char_ = optional . A . char
2021-12-18 10:23:47 +00:00
2022-01-12 17:37:46 +00:00
adminContactReq :: ConnReqContact
adminContactReq =
either error id $ strDecode " https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D "
2023-05-10 15:18:50 +04:00
timeItToView :: ChatMonad' m => String -> m a -> m a
timeItToView s action = do
t1 <- liftIO getCurrentTime
a <- action
t2 <- liftIO getCurrentTime
2023-05-15 21:07:03 +04:00
let diff = diffToMilliseconds $ diffUTCTime t2 t1
2023-05-10 15:18:50 +04:00
toView $ CRTimedAction s diff
pure a