mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-20 06:00:49 +00:00
merge master into badge-in-app-checkout
This commit is contained in:
@@ -29,6 +29,7 @@ import Data.Maybe (fromMaybe, mapMaybe)
|
||||
import Data.Text (Text)
|
||||
import Data.Time.Clock (getCurrentTime, nominalDay)
|
||||
import Simplex.Chat.Badges (badgeServerCredential, defaultFileSizeLimits)
|
||||
import Simplex.Chat.Call (supportedCallVRange)
|
||||
import Simplex.Chat.Controller
|
||||
import Simplex.Chat.Library.Commands
|
||||
import Simplex.Chat.Operators
|
||||
@@ -68,6 +69,7 @@ defaultChatConfig =
|
||||
tbqSize = 1024
|
||||
},
|
||||
chatVRange = supportedChatVRange,
|
||||
callVRange = supportedCallVRange,
|
||||
badgePublicKeys = M.mapKeys fromIntegral entitlementIssuerKeys,
|
||||
badgeServiceAddress = Just $ either error id $ strDecode "https://smp5.simplex.im/a#ooSNWlEZTO2RPE0Ff5ZoybAs5zEhWLMlQrXesnhaZHM",
|
||||
badgeCurrentTime = getCurrentTime,
|
||||
@@ -124,6 +126,7 @@ defaultChatConfig =
|
||||
cleanupManagerInterval = 30 * 60, -- 30 minutes
|
||||
cleanupManagerStepDelay = 3 * 1000000, -- 3 seconds
|
||||
ciExpirationInterval = 30 * 60 * 1000000, -- 30 minutes
|
||||
callInvitationTTL = 180, -- 3 minutes, the apps stop ringing for older invitations
|
||||
highlyAvailable = False,
|
||||
deliveryWorkerDelay = 0,
|
||||
deliveryBucketSize = 10000,
|
||||
|
||||
@@ -124,11 +124,15 @@ deleteStorage = do
|
||||
fs <- lift storageFiles
|
||||
liftIO $ closeDBStore `withStores` fs
|
||||
remove `withDBs` fs
|
||||
removeExported `withDBs` fs
|
||||
removeBackup `withDBs` fs
|
||||
mapM_ removeDir $ filesPath fs
|
||||
mapM_ removeDir $ assetsPath fs
|
||||
mapM_ removeDir =<< chatReadVar tempDirectory
|
||||
where
|
||||
remove f = whenM (doesFileExist f) $ removeFile f
|
||||
removeExported f = remove $ f <> ".exported"
|
||||
removeBackup f = remove $ f <> ".bak"
|
||||
removeDir d = whenM (doesDirectoryExist d) $ removePathForcibly d
|
||||
|
||||
data StorageFiles = StorageFiles
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DerivingStrategies #-}
|
||||
{-# LANGUAGE DerivingVia #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
|
||||
@@ -21,6 +23,7 @@ import Data.ByteString.Char8 (ByteString)
|
||||
import Data.Int (Int64)
|
||||
import Data.Text (Text)
|
||||
import Data.Time.Clock (UTCTime)
|
||||
import Data.Word (Word16)
|
||||
import Simplex.Chat.Options.DB (FromField (..), ToField (..))
|
||||
import Simplex.Chat.Types (Contact, ContactId, User)
|
||||
import Simplex.Messaging.Agent.Store.DB (Binary (..), fromTextField_)
|
||||
@@ -28,6 +31,8 @@ import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fstToLower, singleFieldJSON)
|
||||
import Simplex.Messaging.Util (decodeJSON, encodeJSON)
|
||||
import Simplex.Messaging.Version
|
||||
import Simplex.Messaging.Version.Internal
|
||||
|
||||
data Call = Call
|
||||
{ contactId :: ContactId,
|
||||
@@ -39,6 +44,52 @@ data Call = Call
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
-- Call version history:
|
||||
-- 1 - DH secret is used as media key (also assumed when call messages have no version)
|
||||
-- 2 - media key is derived from DH secret with HKDF (2026-10-01)
|
||||
|
||||
data CallVersion
|
||||
|
||||
instance VersionScope CallVersion
|
||||
|
||||
type VersionCall = Version CallVersion
|
||||
|
||||
type VersionRangeCall = VersionRange CallVersion
|
||||
|
||||
pattern VersionCall :: Word16 -> VersionCall
|
||||
pattern VersionCall v = Version v
|
||||
|
||||
initialCallVersion :: VersionCall
|
||||
initialCallVersion = VersionCall 1
|
||||
|
||||
callMediaKdfVersion :: VersionCall
|
||||
callMediaKdfVersion = VersionCall 2
|
||||
|
||||
-- This should not be used directly in code, instead use `callVRange` from ChatConfig
|
||||
currentCallVersion :: VersionCall
|
||||
currentCallVersion = VersionCall 2
|
||||
|
||||
supportedCallVRange :: VersionRangeCall
|
||||
supportedCallVRange = mkVersionRange initialCallVersion currentCallVersion
|
||||
|
||||
callInitialVRange :: VersionRangeCall
|
||||
callInitialVRange = versionToRange initialCallVersion
|
||||
|
||||
newtype CallVersionRange = CallVersionRange {fromCallVRange :: VersionRangeCall}
|
||||
deriving (Eq, Show)
|
||||
deriving (FromJSON, ToJSON) via (StrJSON "CallVersionRange" VersionRangeCall)
|
||||
|
||||
callMediaKey :: VersionCall -> CallId -> C.PublicKeyX25519 -> C.PrivateKeyX25519 -> C.Key
|
||||
callMediaKey v (CallId salt) peerPubKey privKey
|
||||
| v >= callMediaKdfVersion = C.Key $ C.hkdf salt dhSecret "SimpleXCallMediaKey" callMediaKeySize
|
||||
| otherwise = C.Key dhSecret
|
||||
where
|
||||
dhSecret = C.dhBytes' $ C.dh' peerPubKey privKey
|
||||
|
||||
-- AES-256-GCM key used by the apps for frame encryption
|
||||
callMediaKeySize :: Int
|
||||
callMediaKeySize = 32
|
||||
|
||||
isRcvInvitation :: Call -> Bool
|
||||
isRcvInvitation Call {callState} = case callState of
|
||||
CallInvitationReceived {} -> True
|
||||
@@ -68,7 +119,8 @@ data CallState
|
||||
| CallInvitationReceived
|
||||
{ peerCallType :: CallType,
|
||||
localDhPubKey :: Maybe C.PublicKeyX25519,
|
||||
sharedKey :: Maybe C.Key
|
||||
sharedKey :: Maybe C.Key,
|
||||
callVersion :: Maybe VersionCall
|
||||
}
|
||||
| CallOfferSent
|
||||
{ localCallType :: CallType,
|
||||
@@ -134,7 +186,8 @@ encryptedCall CallType {capabilities = CallCapabilities {encryption}} = encrypti
|
||||
-- | * Types for chat protocol
|
||||
data CallInvitation = CallInvitation
|
||||
{ callType :: CallType,
|
||||
callDhPubKey :: Maybe C.PublicKeyX25519
|
||||
callDhPubKey :: Maybe C.PublicKeyX25519,
|
||||
callVRange :: Maybe CallVersionRange
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
@@ -149,7 +202,8 @@ data CallCapabilities = CallCapabilities
|
||||
data CallOffer = CallOffer
|
||||
{ callType :: CallType,
|
||||
rtcSession :: WebRTCSession,
|
||||
callDhPubKey :: Maybe C.PublicKeyX25519
|
||||
callDhPubKey :: Maybe C.PublicKeyX25519,
|
||||
callVersion :: Maybe VersionCall
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
|
||||
@@ -145,6 +145,7 @@ coreVersionInfo simplexmqCommit =
|
||||
data ChatConfig = ChatConfig
|
||||
{ agentConfig :: AgentConfig,
|
||||
chatVRange :: VersionRangeChat,
|
||||
callVRange :: VersionRangeCall,
|
||||
-- issuer public keys by index: credentials and proofs name the key that signed them, for rotation
|
||||
badgePublicKeys :: Map Int BBSPublicKey,
|
||||
-- Nothing until the badge service is deployed
|
||||
@@ -177,6 +178,7 @@ data ChatConfig = ChatConfig
|
||||
cleanupManagerInterval :: NominalDiffTime,
|
||||
cleanupManagerStepDelay :: Int64,
|
||||
ciExpirationInterval :: Int64, -- microseconds
|
||||
callInvitationTTL :: NominalDiffTime,
|
||||
deliveryWorkerDelay :: Int64, -- microseconds
|
||||
deliveryBucketSize :: Int,
|
||||
webPreviewConfig :: Maybe WebPreviewConfig,
|
||||
@@ -450,7 +452,7 @@ data ChatCommand
|
||||
| APIGetCallInvitations
|
||||
| APICallStatus ContactId WebRTCCallStatus
|
||||
| APIUpdateProfile {userId :: UserId, profile :: Profile}
|
||||
| APISetUserDomain {userId :: UserId, simplexDomain :: Maybe SimplexDomain}
|
||||
| APISetUserDomain {userId :: UserId, simplexDomain :: Maybe (StrJSON "SimplexDomain" SimplexDomain)}
|
||||
| APISetContactPrefs {contactId :: ContactId, preferences :: Preferences}
|
||||
| APISetContactAlias {contactId :: ContactId, localAlias :: LocalAlias}
|
||||
| APISetGroupAlias {groupId :: GroupId, localAlias :: LocalAlias}
|
||||
|
||||
@@ -13,12 +13,15 @@ module Simplex.Chat.Core
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Concurrent (forkIO)
|
||||
import Control.Exception (fromException, mask, onException, throwTo)
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.Reader
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.List (find)
|
||||
import Data.Maybe (isNothing)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
@@ -90,12 +93,22 @@ simplexChatCore cfg@ChatConfig {confirmMigrations, testView, chatHooks} opts@Cha
|
||||
runSimplexChat :: ChatConfig -> ChatOpts -> User -> ChatController -> (User -> ChatController -> IO ()) -> IO ()
|
||||
runSimplexChat ChatConfig {testView} ChatOpts {coreOptions = CoreChatOpts {chatRelay, chatRelayServer, headless, maintenance}} u cc@ChatController {config = ChatConfig {chatHooks}} chat
|
||||
| maintenance = wait =<< async (chat u cc)
|
||||
| otherwise = flip finally (stopChatController cc) $ do
|
||||
a1 <- runReaderT (startChatController True True False) cc
|
||||
when (chatRelay && not testView) $ askCreateRelayAddress cc u chatRelayServer headless
|
||||
forM_ (postStartHook chatHooks) ($ cc)
|
||||
a2 <- async $ chat u cc
|
||||
waitEither_ a1 a2
|
||||
| otherwise = do
|
||||
a1 <- runReaderT (startChatController True True False) cc `onException` stopChatController cc
|
||||
flip finally (stopUnlessCancelled a1) $ do
|
||||
when (chatRelay && not testView) $ askCreateRelayAddress cc u chatRelayServer headless
|
||||
forM_ (postStartHook chatHooks) ($ cc)
|
||||
-- throwTo waits while the callback is masked, so an outside interrupt cancels it from a forked thread.
|
||||
-- A /_stop sent from the callback ends a1 first, which leaves the callback running.
|
||||
mask $ \restore -> do
|
||||
a2 <- asyncWithUnmask $ \unmask -> unmask (chat u cc)
|
||||
let cancelCallback = poll a1 >>= \r -> when (isNothing r) $ void $ forkIO $ throwTo (asyncThreadId a2) AsyncCancelled
|
||||
restore (waitEither_ a1 a2) `onException` cancelCallback
|
||||
where
|
||||
stopUnlessCancelled a1 =
|
||||
poll a1 >>= \case
|
||||
Just (Left e) | fromException e == Just AsyncCancelled -> pure ()
|
||||
_ -> stopChatController cc
|
||||
|
||||
sendChatCmdStr :: ChatController -> String -> IO (Either ChatError ChatResponse)
|
||||
sendChatCmdStr cc s = runReaderT (execChatCommand CSLocal (encodeUtf8 $ T.pack s) 0) cc
|
||||
|
||||
@@ -359,7 +359,9 @@ startReceiveUserFiles user = do
|
||||
|
||||
restoreCalls :: CM' ()
|
||||
restoreCalls = do
|
||||
savedCalls <- fromRight [] <$> runExceptT (withFastStore' getCalls)
|
||||
ttl <- asks (callInvitationTTL . config)
|
||||
cutoffTs <- addUTCTime (-ttl) <$> liftIO getCurrentTime
|
||||
savedCalls <- fromRight [] <$> runExceptT (withFastStore' $ \db -> expireCalls db cutoffTs >> getCalls db)
|
||||
let callsMap = M.fromList $ map (\call@Call {contactId} -> (contactId, call)) savedCalls
|
||||
calls <- asks currentCalls
|
||||
atomically $ writeTVar calls callsMap
|
||||
@@ -1519,7 +1521,8 @@ processChatCommand cxt nm = \case
|
||||
callId <- atomically $ CallId <$> C.randomBytes 16 g
|
||||
callUUID <- UUID.toText <$> liftIO V4.nextRandom
|
||||
dhKeyPair <- atomically $ if encryptedCall callType then Just <$> C.generateKeyPair g else pure Nothing
|
||||
let invitation = CallInvitation {callType, callDhPubKey = fst <$> dhKeyPair}
|
||||
ChatConfig {callVRange = callVR} <- asks config
|
||||
let invitation = CallInvitation {callType, callDhPubKey = fst <$> dhKeyPair, callVRange = Just $ CallVersionRange callVR}
|
||||
callState = CallInvitationSent {localCallType = callType, localDhPrivKey = snd <$> dhKeyPair}
|
||||
(msg, _) <- sendDirectContactMessage user ct (XCallInv callId invitation)
|
||||
ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndCall CISCallPending 0)
|
||||
@@ -1547,9 +1550,9 @@ processChatCommand cxt nm = \case
|
||||
APISendCallOffer contactId WebRTCCallOffer {callType, rtcSession} ->
|
||||
-- party accepting call
|
||||
withCurrentCall contactId $ \user ct call@Call {callId, chatItemId, callState} -> case callState of
|
||||
CallInvitationReceived {peerCallType, localDhPubKey, sharedKey} -> do
|
||||
CallInvitationReceived {peerCallType, localDhPubKey, sharedKey, callVersion} -> do
|
||||
let callDhPubKey = if encryptedCall callType then localDhPubKey else Nothing
|
||||
offer = CallOffer {callType, rtcSession, callDhPubKey}
|
||||
offer = CallOffer {callType, rtcSession, callDhPubKey, callVersion}
|
||||
callState' = CallOfferSent {localCallType = callType, peerCallType, localCallSession = rtcSession, sharedKey}
|
||||
aciContent = ACIContent SMDRcv $ CIRcvCall CISCallAccepted 0
|
||||
(SndMessage {msgId}, _) <- sendDirectContactMessage user ct (XCallOffer callId offer)
|
||||
@@ -1604,7 +1607,8 @@ processChatCommand cxt nm = \case
|
||||
withCurrentCall contactId $ \user ct call ->
|
||||
updateCallItemStatus user ct call receivedStatus Nothing $> Just call
|
||||
APIUpdateProfile userId profile -> withUserId userId (`updateProfile` profile)
|
||||
APISetUserDomain userId domain_ -> withUserId userId $ \user@User {profile = p@LocalProfile {contactLink, contactDomain}} ->
|
||||
APISetUserDomain userId strDomain_ -> withUserId userId $ \user@User {profile = p@LocalProfile {contactLink, contactDomain}} -> do
|
||||
let domain_ = unStrJSON <$> strDomain_
|
||||
if (claimDomain <$> contactDomain) == domain_
|
||||
then pure $ CRUserProfileNoChange user
|
||||
else do
|
||||
@@ -2876,7 +2880,8 @@ processChatCommand cxt nm = \case
|
||||
Nothing -> throwChatError $ CEContactNotActive ct
|
||||
APIAcceptMember groupId gmId role -> withUser $ \user@User {userId} -> do
|
||||
(g@(GIK gInfo _), m) <- withFastStore $ \db -> (,) <$> getGroupInfoKeys db cxt user groupId <*> getGroupMemberById db cxt user gmId
|
||||
assertUserGroupRole gInfo $ max GRModerator role
|
||||
-- same rule as role change (moderators grant up to member); pending member's role is a stand-in, so treat it as member
|
||||
assertUserGroupRole gInfo $ roleRequiredToChange GRMember role
|
||||
case memberStatus m of
|
||||
GSMemPendingApproval | memberCategory m == GCInviteeMember -> do -- only host can approve
|
||||
let GroupInfo {groupProfile = GroupProfile {memberAdmission}} = gInfo
|
||||
@@ -2955,6 +2960,8 @@ processChatCommand cxt nm = \case
|
||||
throwCmdError "can't change role of multiple members when admins selected, or new role is admin"
|
||||
when anyPending $ throwCmdError "can't change role of members pending approval"
|
||||
when (anyRelay || newRole == GRRelay) $ throwCmdError "relay role can't be changed"
|
||||
-- TODO [multi-owner] allow once owners are added via link data - until then promoted owner would lack link authority
|
||||
when (useRelays' gInfo && newRole == GROwner) $ throwCmdError "owner role can't be assigned in channels"
|
||||
-- TODO allow moderators (needs UI) - relay is rejected above (anyRelay), so drop the GRAdmin floor:
|
||||
-- TODO assertUserGroupRole gInfo (roleRequiredToChange maxRole newRole)
|
||||
assertUserGroupRole gInfo $ maximum ([GRAdmin, maxRole, newRole] :: [GroupMemberRole])
|
||||
@@ -3999,7 +4006,7 @@ processChatCommand cxt nm = \case
|
||||
dm <- case gInfo_ of
|
||||
Just (Just gInfo@(GIK g gks))
|
||||
| useRelays' g -> case relayMemberId_ of
|
||||
Just relayMemberId -> encodeXMemberConnInfo gInfo relayMemberId profileToSend
|
||||
Just relayMemberId -> encodeXMemberConnInfo pqSup gInfo relayMemberId profileToSend
|
||||
Nothing -> throwChatError $ CEInternalError "relay group join without target relay memberId"
|
||||
| otherwise -> encodeConnInfoPQ pqSup $ XContact profileToSend (Just $ groupMemberKey gks) (Just xContactId) welcomeSharedMsgId msg_
|
||||
_ ->
|
||||
@@ -6298,7 +6305,7 @@ chatCommandP =
|
||||
"/_call status @" *> (APICallStatus <$> A.decimal <* A.space <*> strP),
|
||||
"/_call get" $> APIGetCallInvitations,
|
||||
"/_profile " *> (APIUpdateProfile <$> A.decimal <* A.space <*> jsonP),
|
||||
"/_set domain " *> (APISetUserDomain <$> A.decimal <*> optional (A.space *> strP)),
|
||||
"/_set domain " *> (APISetUserDomain <$> A.decimal <*> optional (A.space *> (StrJSON <$> strP))),
|
||||
"/_set alias @" *> (APISetContactAlias <$> A.decimal <*> (A.space *> textP <|> pure "")),
|
||||
"/_set alias #" *> (APISetGroupAlias <$> A.decimal <*> (A.space *> textP <|> pure "")),
|
||||
"/_set alias :" *> (APISetConnectionAlias <$> A.decimal <*> (A.space *> textP <|> pure "")),
|
||||
|
||||
@@ -443,10 +443,11 @@ xftpSndFileTransfer_ user file@(CryptoFile filePath cfArgs) fileSize n contactOr
|
||||
fInv = (xftpFileInvitation fileName fileSize dummyFileDescr :: FileInvitation) {fileBadge}
|
||||
fsFilePath <- lift $ toFSFilePath filePath
|
||||
let srcFile = CryptoFile fsFilePath cfArgs
|
||||
aFileId <- withAgent $ \a -> xftpSendFile a (aUserId user) srcFile (roundedFDCount n) Nothing
|
||||
aFileId <- withAgent $ \a -> xftpPrepareSendFile a (aUserId user) srcFile (roundedFDCount n) Nothing
|
||||
-- TODO CRSndFileStart event for XFTP
|
||||
chSize <- asks $ fileChunkSize . config
|
||||
ft@FileTransferMeta {fileId} <- withStore' $ \db -> createSndFileTransferXFTP db user contactOrGroup_ file fInv (AgentSndFileId aFileId) Nothing chSize
|
||||
withAgent (`xftpStartSendFile` aFileId)
|
||||
let fileSource = Just $ CryptoFile filePath cfArgs
|
||||
ciFile = CIFile {fileId, fileName, fileSize, fileSource, fileStatus = CIFSSndStored, fileProtocol = FPXFTP, fileExpires = Nothing, fileProhibited = Nothing}
|
||||
pure (fInv, ciFile, ft)
|
||||
@@ -485,9 +486,10 @@ xftpSndFileRedirect user ftId vfd = do
|
||||
let fileName = "redirect.yaml"
|
||||
file = CryptoFile fileName Nothing
|
||||
fInv = xftpFileInvitation fileName (fromIntegral $ B.length $ strEncode vfd) dummyFileDescr
|
||||
aFileId <- withAgent $ \a -> xftpSendDescription a (aUserId user) vfd (roundedFDCount 1)
|
||||
aFileId <- withAgent $ \a -> xftpPrepareSendDescription a (aUserId user) vfd (roundedFDCount 1)
|
||||
chSize <- asks $ fileChunkSize . config
|
||||
withStore' $ \db -> createSndFileTransferXFTP db user Nothing file fInv (AgentSndFileId aFileId) (Just ftId) chSize
|
||||
ft <- withStore' $ \db -> createSndFileTransferXFTP db user Nothing file fInv (AgentSndFileId aFileId) (Just ftId) chSize
|
||||
ft <$ withAgent (`xftpStartSendFile` aFileId)
|
||||
|
||||
dummyFileDescr :: FileDescr
|
||||
dummyFileDescr = FileDescr {fileDescrText = "", fileDescrPartNo = 0, fileDescrComplete = False}
|
||||
@@ -838,6 +840,16 @@ receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete}
|
||||
rcvSize = max (toInteger encSize) redirectSize
|
||||
-- 10 MB margin: encryption and chunk-size rounding make the transfer larger than the advertised size
|
||||
maxRcvSize = min expectedFileSize (toInteger FD.maxFileSizeHard) + toInteger (FD.mb 10 :: Int64)
|
||||
-- TODO re-enable redirects with relay checks
|
||||
when (isJust redirect) $ do
|
||||
cxt <- chatStoreCxt
|
||||
aci_ <- withStore $ \db -> do
|
||||
liftIO $ updateFileCancelled db user fileId (CIFSRcvError $ FileErrOther "redirect not allowed")
|
||||
lookupChatItemByFileId db cxt user fileId
|
||||
forM_ aci_ $ \aci -> do
|
||||
cleanupACIFile aci
|
||||
toView $ CEvtChatItemUpdated user aci
|
||||
throwChatError $ CEInvalidFileDescription "redirect not allowed"
|
||||
when (rcvSize > maxRcvSize) $ throwChatError $ CEFileRcvChunk "declared file size exceeds the file invitation size"
|
||||
if userApprovedRelays
|
||||
then receive' rd True
|
||||
@@ -852,9 +864,10 @@ receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete}
|
||||
where
|
||||
receive' :: ValidFileDescription 'FRecipient -> Bool -> CM ()
|
||||
receive' rd approved = do
|
||||
aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) rd cfArgs approved
|
||||
aFileId <- withAgent $ \a -> xftpPrepareReceiveFile a (aUserId user) rd cfArgs approved
|
||||
startReceivingFile user fileId
|
||||
withStore' $ \db -> updateRcvFileAgentId db fileId (Just $ AgentRcvFileId aFileId)
|
||||
withAgent (`xftpStartReceiveFile` aFileId)
|
||||
getUnknownSrvs :: [XFTPServer] -> CM [XFTPServer]
|
||||
getUnknownSrvs srvs = do
|
||||
knownSrvs <- L.map protoServer' <$> getKnownAgentServers SPXFTP user
|
||||
@@ -905,13 +918,14 @@ receiveViaURI :: User -> FileDescriptionURI -> CryptoFile -> CM RcvFileTransfer
|
||||
receiveViaURI user@User {userId} FileDescriptionURI {description} cf@CryptoFile {cryptoArgs} = do
|
||||
fileId <- withStore $ \db -> createRcvStandaloneFileTransfer db userId cf fileSize chunkSize
|
||||
-- currently the only use case is user migrating via their configured servers, so we pass approvedRelays = True
|
||||
aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) description cryptoArgs True
|
||||
withStore $ \db -> do
|
||||
aFileId <- withAgent $ \a -> xftpPrepareReceiveFile a (aUserId user) description cryptoArgs True
|
||||
ft <- withStore $ \db -> do
|
||||
liftIO $ do
|
||||
updateRcvFileStatus db fileId FSConnected
|
||||
updateCIFileStatus db user fileId $ CIFSRcvTransfer 0 1
|
||||
updateRcvFileAgentId db fileId (Just $ AgentRcvFileId aFileId)
|
||||
getRcvFileTransfer db user fileId
|
||||
ft <$ withAgent (`xftpStartReceiveFile` aFileId)
|
||||
where
|
||||
FD.ValidFileDescription FD.FileDescription {size = FD.FileSize fileSize, chunkSize = FD.FileSize chunkSize} = description
|
||||
|
||||
@@ -2434,6 +2448,19 @@ batchSendConnMessagesB mode _user conn msgFlags msgs_ = do
|
||||
batchSndMessagesJSON :: BatchMode -> NonEmpty (Either ChatError SndMessage) -> [Either ChatError MsgBatch]
|
||||
batchSndMessagesJSON mode = batchMessages mode maxEncodedMsgLength . L.toList
|
||||
|
||||
compressToLimit :: MonadError ChatError m => Int -> MsgBody -> m MsgBody
|
||||
compressToLimit maxLen s
|
||||
| B.length s <= maxLen = pure s
|
||||
| B.length s' <= maxLen = pure s'
|
||||
| otherwise = throwError $ ChatError $ CEException "large compressed body"
|
||||
where
|
||||
s' = compressedBatchMsgBody_ s
|
||||
|
||||
compressConnInfo :: PQSupport -> MsgBody -> CM MsgBody
|
||||
compressConnInfo pqSup = compressToLimit $ case pqSup of
|
||||
PQSupportOn -> maxEncodedInfoLengthPQ
|
||||
PQSupportOff -> maxEncodedInfoLength
|
||||
|
||||
encodeConnInfo :: MsgEncodingI e => ChatMsgEvent e -> CM ByteString
|
||||
encodeConnInfo = encodeConnInfoPQ PQSupportOff
|
||||
|
||||
@@ -2442,32 +2469,27 @@ encodeConnInfoPQ pqSup chatMsgEvent = do
|
||||
cxt <- chatStoreCxt
|
||||
let info = ChatMessage {chatVRange = vr cxt, msgId = Nothing, chatMsgEvent}
|
||||
case encodeChatMessage maxEncodedInfoLength info of
|
||||
ECMEncoded connInfo -> case pqSup of
|
||||
PQSupportOn | B.length connInfo > maxCompressedInfoLength -> do
|
||||
let connInfo' = compressedBatchMsgBody_ connInfo
|
||||
when (B.length connInfo' > maxCompressedInfoLength) $ throwChatError $ CEException "large compressed info"
|
||||
pure connInfo'
|
||||
_ -> pure connInfo
|
||||
ECMEncoded connInfo -> compressConnInfo pqSup connInfo
|
||||
ECMLarge -> throwChatError $ CEException "large info"
|
||||
|
||||
-- conn-info wrapped as a signed element, so the receiver can verify the signature over the body
|
||||
encodeSignedConnInfo :: MsgEncodingI e => MsgSigning -> ChatMsgEvent e -> CM ByteString
|
||||
encodeSignedConnInfo signing chatMsgEvent = do
|
||||
encodeSignedConnInfo :: MsgEncodingI e => PQSupport -> MsgSigning -> ChatMsgEvent e -> CM ByteString
|
||||
encodeSignedConnInfo pqSup signing chatMsgEvent = do
|
||||
vr <- chatVersionRange
|
||||
let info = ChatMessage {chatVRange = vr, msgId = Nothing, chatMsgEvent}
|
||||
case encodeChatMessage maxEncodedInfoLength info of
|
||||
ECMEncoded body -> pure $ encodeBatchElement (Just $ signChatMsgBody signing body) body
|
||||
ECMEncoded body -> compressConnInfo pqSup $ encodeBatchElement (Just $ signChatMsgBody signing body) body
|
||||
ECMLarge -> throwChatError $ CEException "large signed info"
|
||||
|
||||
-- signed XMember for a relay-group join: proves the joiner holds the member key it asserts, and carries
|
||||
-- viaRelay = the target relay's memberId inside the signed body so a sibling relay can't accept a replay
|
||||
encodeXMemberConnInfo :: GroupInfoKeys -> MemberId -> Profile -> CM ByteString
|
||||
encodeXMemberConnInfo (GIK gInfo@GroupInfo {membership = GroupMember {memberId}} gks) relayMemberId profileToSend =
|
||||
encodeXMemberConnInfo :: PQSupport -> GroupInfoKeys -> MemberId -> Profile -> CM ByteString
|
||||
encodeXMemberConnInfo pqSup (GIK gInfo@GroupInfo {membership = GroupMember {memberId}} gks) relayMemberId profileToSend =
|
||||
let memberPrivKey' = memberPrivKey gks
|
||||
xMemberEvt = XMember profileToSend memberId (MemberKey $ C.publicKey memberPrivKey') (Just relayMemberId)
|
||||
bindingData = groupBindingData gInfo memberId (C.publicKey memberPrivKey')
|
||||
signing = MsgSigning CBGroup bindingData KRMember memberPrivKey'
|
||||
in encodeSignedConnInfo signing xMemberEvt
|
||||
in encodeSignedConnInfo pqSup signing xMemberEvt
|
||||
|
||||
deliverMessage :: Connection -> CMEventTag e -> MsgBody -> MessageId -> CM (Int64, PQEncryption)
|
||||
deliverMessage conn cmEventTag msgBody msgId = do
|
||||
@@ -2491,21 +2513,20 @@ deliverMessages msgs = deliverMessagesB $ L.map Right msgs
|
||||
|
||||
deliverMessagesB :: NonEmpty (Either ChatError ChatMsgReq) -> CM (NonEmpty (Either ChatError ([Int64], PQEncryption)))
|
||||
deliverMessagesB msgReqs = do
|
||||
msgReqs' <- if any connSupportsPQ msgReqs then liftIO compressBodies else pure msgReqs
|
||||
msgReqs' <- liftIO compressBodies
|
||||
sent <- L.zipWith prepareBatch msgReqs' <$> withAgent (`sendMessagesB` snd (mapAccumL toAgent Nothing msgReqs'))
|
||||
lift . void $ withStoreBatch' $ \db -> map (updatePQSndEnabled db) (rights . L.toList $ sent)
|
||||
lift . withStoreBatch $ \db -> L.map (bindRight $ createDelivery db) sent
|
||||
where
|
||||
-- group sends share bodies between connections via VRRef, so the smallest limit applies to the batch
|
||||
maxLen = if any connSupportsPQ msgReqs then maxEncodedMsgLengthPQ else maxEncodedMsgLength
|
||||
connSupportsPQ = \case
|
||||
Right (Connection {pqSupport = PQSupportOn}, _, _) -> True
|
||||
_ -> False
|
||||
compressBodies =
|
||||
forME msgReqs $ \(conn, msgFlags, (mbr, msgIds)) -> runExceptT $ do
|
||||
mbr' <- case mbr of
|
||||
VRValue i msgBody | B.length msgBody > maxCompressedMsgLength -> do
|
||||
let msgBody' = compressedBatchMsgBody_ msgBody
|
||||
when (B.length msgBody' > maxCompressedMsgLength) $ throwError $ ChatError $ CEException "large compressed message"
|
||||
pure $ VRValue i msgBody'
|
||||
VRValue i msgBody -> VRValue i <$> compressToLimit maxLen msgBody
|
||||
v -> pure v
|
||||
pure (conn, msgFlags, (mbr', msgIds))
|
||||
toAgent prev = \case
|
||||
@@ -3041,7 +3062,7 @@ allowAgentConnectionAsync user conn@Connection {pqSupport} confId gInfo_ msg = d
|
||||
Just gInfo@(GIK g _) | useRelays' g || maxVersion (peerChatVRange conn) >= relayWebCapVersion -> groupMsgSigning False gInfo msg
|
||||
_ -> Nothing
|
||||
dm <- case signing_ of
|
||||
Just signing -> encodeSignedConnInfo signing msg
|
||||
Just signing -> encodeSignedConnInfo pqSupport signing msg
|
||||
Nothing -> encodeConnInfoPQ pqSupport msg
|
||||
allowAgentConnectionInfo user conn confId dm
|
||||
|
||||
|
||||
@@ -659,7 +659,6 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
|
||||
ct' = ct {activeConn = Just conn'} :: Contact
|
||||
-- [incognito] print incognito profile used for this contact
|
||||
incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId)
|
||||
toView $ CEvtContactConnected user ct' (fmap fromLocalProfile incognitoProfile)
|
||||
let createE2EItem = createInternalChatItem user (CDDirectRcv ct') (CIRcvDirectE2EEInfo $ e2eInfoEncrypted $ Just pqEnc) Nothing
|
||||
-- TODO [short links] get contact request by contactRequestId, check encryption (UserContactRequest.pqSupport)?
|
||||
when (directOrUsed ct') $ case (preparedContact ct', contactRequestId' ct') of
|
||||
@@ -674,6 +673,7 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
|
||||
withStore' (\db -> getContactRequest' db user connReqId) >>= \case
|
||||
Just UserContactRequest {pqSupport} | CR.pqSupportToEnc pqSupport == pqEnc -> pure ()
|
||||
_ -> createE2EItem
|
||||
toView $ CEvtContactConnected user ct' (fmap fromLocalProfile incognitoProfile)
|
||||
when (contactConnInitiated conn') $ do
|
||||
probeMatchingMembers ct' (contactConnIncognito ct')
|
||||
withStore' $ \db -> resetContactConnInitiated db user conn'
|
||||
@@ -884,7 +884,6 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
|
||||
pure gInfo {membership = membership {memberStatus = GSMemConnected}}
|
||||
else pure gInfo
|
||||
pure (m {memberStatus = GSMemConnected}, gInfo')
|
||||
toView $ CEvtUserJoinedGroup user gInfo' m'
|
||||
when (isRelay membership) $ do
|
||||
cc <- ask
|
||||
atomically $ channelProfileUpdated cc groupId groupProfile
|
||||
@@ -898,10 +897,14 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
|
||||
let prepared = preparedGroup gInfo''
|
||||
unless (isJust prepared) $ createGroupFeatureItems user cd CIRcvGroupFeature gInfo''
|
||||
memberConnectedChatItem gInfo'' scopeInfo m''
|
||||
toView $ CEvtUserJoinedGroup user gInfo' m'
|
||||
let welcomeMsgId_ = (\PreparedGroup {welcomeSharedMsgId = mId} -> mId) <$> prepared
|
||||
unless (memberPending membership || isJust welcomeMsgId_) $ maybeCreateGroupDescrLocal gInfo'' m''
|
||||
)
|
||||
(memberConnectedChatItem gInfo'' scopeInfo m'')
|
||||
( do
|
||||
memberConnectedChatItem gInfo'' scopeInfo m''
|
||||
toView $ CEvtUserJoinedGroup user gInfo' m'
|
||||
)
|
||||
where
|
||||
firstConnectedHost
|
||||
| useRelays' gInfo = do
|
||||
@@ -1247,7 +1250,7 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
|
||||
withStore' $ \db -> updateConnLinkData db user conn cReq cReqHash groupLinkId chatV pqSup
|
||||
let incognitoProfile = fromLocalProfile <$> incognitoMembershipProfile gInfo
|
||||
profileToSend <- presentUserBadge user incognitoProfile $ userProfileInGroup user gInfo incognitoProfile
|
||||
dm <- encodeXMemberConnInfo g relayMemberId profileToSend
|
||||
dm <- encodeXMemberConnInfo pqSup g relayMemberId profileToSend
|
||||
subMode <- chatReadVar subscriptionMode
|
||||
(cmdId, connId') <- prepareAgentJoin user (Just conn) True cReq
|
||||
joinAgentConnectionAsync cmdId True connId' True cReq dm subMode
|
||||
@@ -2826,7 +2829,8 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
|
||||
|
||||
xGrpLinkAcpt :: GroupInfoKeys -> GroupMember -> GroupAcceptance -> GroupMemberRole -> MemberId -> RcvMessage -> UTCTime -> CM ()
|
||||
xGrpLinkAcpt g@(GIK gInfo@GroupInfo {membership} _) m acceptance role memberId msg brokerTs
|
||||
| memberRole' m < GRModerator || memberRole' m < role =
|
||||
-- same rule as role change (moderators grant up to member); pending member's role is a stand-in, so treat it as member
|
||||
| memberRole' m < roleRequiredToChange GRMember role =
|
||||
messageError "x.grp.link.acpt with insufficient member permissions"
|
||||
| sameMemberId memberId membership = processUserAccepted
|
||||
| otherwise =
|
||||
@@ -2896,7 +2900,7 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
|
||||
pure m'
|
||||
Just mContactId -> do
|
||||
mCt <- withStore $ \db -> getContact db cxt user mContactId
|
||||
if canUpdateProfile mCt
|
||||
if contactUpdatableFromMember mCt
|
||||
then do
|
||||
(m', ct') <- withStore $ \db -> updateContactMemberProfile db cxt user m mCt p'
|
||||
unless (muteEventInChannel gInfo m') $ do
|
||||
@@ -2905,12 +2909,6 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
|
||||
toView $ CEvtContactUpdated user mCt ct'
|
||||
pure m'
|
||||
else pure m
|
||||
where
|
||||
canUpdateProfile ct
|
||||
| not (contactActive ct) = True
|
||||
| otherwise = case contactConn ct of
|
||||
Nothing -> True
|
||||
Just conn -> not (connReady conn) || (authErrCounter conn >= 1)
|
||||
| otherwise =
|
||||
pure m
|
||||
where
|
||||
@@ -3009,15 +3007,23 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
|
||||
|
||||
-- to party accepting call
|
||||
xCallInv :: Contact -> CallId -> CallInvitation -> RcvMessage -> MsgMeta -> CM ()
|
||||
xCallInv ct@Contact {contactId} callId CallInvitation {callType, callDhPubKey} msg@RcvMessage {sharedMsgId_} msgMeta = do
|
||||
xCallInv ct@Contact {contactId} callId CallInvitation {callType, callDhPubKey, callVRange = peerCallVRange} msg@RcvMessage {sharedMsgId_} msgMeta = do
|
||||
if featureAllowed SCFCalls forContact ct
|
||||
then do
|
||||
ChatConfig {callVRange = callVR} <- asks config
|
||||
case callVR `compatibleVersion` maybe callInitialVRange fromCallVRange peerCallVRange of
|
||||
Just (Compatible callVersion) -> createCallInvitation callVersion
|
||||
Nothing -> messageError "x.call.inv: incompatible call version"
|
||||
else featureRejected CFCalls
|
||||
where
|
||||
brokerTs = metaBrokerTs msgMeta
|
||||
createCallInvitation callVersion = do
|
||||
g <- asks random
|
||||
dhKeyPair <- atomically $ if encryptedCall callType then Just <$> C.generateKeyPair g else pure Nothing
|
||||
(ci, cInfo) <- saveCallItem CISCallPending
|
||||
callUUID <- UUID.toText <$> liftIO V4.nextRandom
|
||||
let sharedKey = C.Key . C.dhBytes' <$> (C.dh' <$> callDhPubKey <*> (snd <$> dhKeyPair))
|
||||
callState = CallInvitationReceived {peerCallType = callType, localDhPubKey = fst <$> dhKeyPair, sharedKey}
|
||||
let sharedKey = callMediaKey callVersion callId <$> callDhPubKey <*> (snd <$> dhKeyPair)
|
||||
callState = CallInvitationReceived {peerCallType = callType, localDhPubKey = fst <$> dhKeyPair, sharedKey, callVersion = Just callVersion}
|
||||
call' = Call {contactId, callId, callUUID, 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
|
||||
@@ -3028,9 +3034,6 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
|
||||
forM_ call_ $ \call -> updateCallItemStatus user ct call WCSDisconnected Nothing
|
||||
toView $ CEvtCallInvitation RcvCallInvitation {user, contact = ct, callType, sharedKey, callUUID, callTs = chatItemTs' ci}
|
||||
toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv cInfo ci]
|
||||
else featureRejected CFCalls
|
||||
where
|
||||
brokerTs = metaBrokerTs msgMeta
|
||||
saveCallItem status = saveRcvChatItemNoParse user (CDDirectRcv ct) msg brokerTs (CIRcvCall status 0)
|
||||
featureRejected f = do
|
||||
let content = ciContentNoParse $ CIRcvChatFeatureRejected f
|
||||
@@ -3039,18 +3042,25 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
|
||||
|
||||
-- to party initiating call
|
||||
xCallOffer :: Contact -> CallId -> CallOffer -> RcvMessage -> CM ()
|
||||
xCallOffer ct callId CallOffer {callType, rtcSession, callDhPubKey} msg = do
|
||||
xCallOffer ct callId CallOffer {callType, rtcSession, callDhPubKey, callVersion = peerCallVersion} msg = do
|
||||
ChatConfig {callVRange = callVR} <- asks config
|
||||
msgCurrentCall ct callId "x.call.offer" msg $
|
||||
\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}
|
||||
askConfirmation = encryptedCall localCallType && not (encryptedCall callType)
|
||||
toView CEvtCallOffer {user, contact = ct, callType, offer = rtcSession, sharedKey, askConfirmation}
|
||||
pure (Just call {callState = callState'}, Just . ACIContent SMDSnd $ CISndCall CISCallAccepted 0)
|
||||
CallInvitationSent {localCallType, localDhPrivKey}
|
||||
| callVersion `isCompatible` callVR -> do
|
||||
let sharedKey = callMediaKey callVersion callId <$> callDhPubKey <*> localDhPrivKey
|
||||
callState' = CallOfferReceived {localCallType, peerCallType = callType, peerCallSession = rtcSession, sharedKey}
|
||||
askConfirmation = encryptedCall localCallType && isNothing sharedKey
|
||||
toView CEvtCallOffer {user, contact = ct, callType, offer = rtcSession, sharedKey, askConfirmation}
|
||||
pure (Just call {callState = callState'}, Just . ACIContent SMDSnd $ CISndCall CISCallAccepted 0)
|
||||
| otherwise -> do
|
||||
messageError "x.call.offer: unsupported call version"
|
||||
pure (Just call, Nothing)
|
||||
_ -> do
|
||||
msgCallStateError "x.call.offer" call
|
||||
pure (Just call, Nothing)
|
||||
where
|
||||
callVersion = fromMaybe initialCallVersion peerCallVersion
|
||||
|
||||
-- to party accepting call
|
||||
xCallAnswer :: Contact -> CallId -> CallAnswer -> RcvMessage -> CM ()
|
||||
|
||||
@@ -518,11 +518,11 @@ displayNameTextP_ = (,"") <$> quoted '\'' <|> splitPunctuation <$> takeNameTill
|
||||
refChar c = c > ' ' && c /= '#' && c /= '@' && c /= '\''
|
||||
|
||||
commandTextP :: Parser (Text, Text)
|
||||
commandTextP = do
|
||||
(cmd, punct) <- displayNameTextP_
|
||||
case T.words cmd of
|
||||
(keyword : _) | T.all (\c -> isAlpha c || isDigit c || c == '_') keyword -> pure (cmd, punct)
|
||||
_ -> fail "invalid command keyword"
|
||||
commandTextP = commandText <$> displayNameTextP_
|
||||
where
|
||||
commandText (cmd, punct)
|
||||
| T.null cmd = (punct, "")
|
||||
| otherwise = (cmd, punct)
|
||||
|
||||
splitPunctuation :: Text -> (Text, Text)
|
||||
splitPunctuation s = (T.dropWhileEnd isPunctuation s, T.takeWhileEnd isPunctuation s)
|
||||
|
||||
@@ -92,7 +92,7 @@ import Simplex.Messaging.Version hiding (version)
|
||||
-- This indirection is needed for backward/forward compatibility testing.
|
||||
-- Testing with real app versions is still needed, as tests use the current code with different version ranges, not the old code.
|
||||
currentChatVersion :: VersionChat
|
||||
currentChatVersion = VersionChat 20
|
||||
currentChatVersion = VersionChat 21
|
||||
|
||||
-- This should not be used directly in code, instead use `chatVRange` from ChatConfig (see comment above)
|
||||
supportedChatVRange :: VersionRangeChat
|
||||
@@ -140,6 +140,9 @@ groupRosterVersion = VersionChat 19
|
||||
groupMemberKeyVersion :: VersionChat
|
||||
groupMemberKeyVersion = VersionChat 20
|
||||
|
||||
anyTextCommandsVersion :: VersionChat
|
||||
anyTextCommandsVersion = VersionChat 21
|
||||
|
||||
data ConnectionEntity
|
||||
= RcvDirectMsgConnection {entityConnection :: Connection, contact :: Maybe Contact}
|
||||
| RcvGroupMsgConnection {entityConnection :: Connection, groupInfo :: GroupInfo, groupMember :: GroupMember}
|
||||
@@ -922,8 +925,8 @@ maxEncodedMsgLength :: Int
|
||||
maxEncodedMsgLength = 15602
|
||||
|
||||
-- maxEncodedMsgLength - 2222, see e2eEncUserMsgLength in agent
|
||||
maxCompressedMsgLength :: Int
|
||||
maxCompressedMsgLength = 13380
|
||||
maxEncodedMsgLengthPQ :: Int
|
||||
maxEncodedMsgLengthPQ = 13380
|
||||
|
||||
maxDecompressedMsgLength :: Int
|
||||
maxDecompressedMsgLength = 65536
|
||||
@@ -932,6 +935,9 @@ maxDecompressedMsgLength = 65536
|
||||
maxBatchElementCount :: Int
|
||||
maxBatchElementCount = 255
|
||||
|
||||
maxFwdDepth :: Int
|
||||
maxFwdDepth = 1
|
||||
|
||||
-- Defensive entry-count bound for the roster blob parser (rosterBlobP) and the
|
||||
-- promotion cap over the promoted (member/moderator/admin) set.
|
||||
maxGroupRosterSize :: Int
|
||||
@@ -959,8 +965,8 @@ rosterBlobP = do
|
||||
maxEncodedInfoLength :: Int
|
||||
maxEncodedInfoLength = 14694
|
||||
|
||||
maxCompressedInfoLength :: Int
|
||||
maxCompressedInfoLength = 10968 -- maxEncodedInfoLength - 3726, see e2eEncConnInfoLength in agent
|
||||
maxEncodedInfoLengthPQ :: Int
|
||||
maxEncodedInfoLengthPQ = 10968 -- maxEncodedInfoLength - 3726, see e2eEncConnInfoLength in agent
|
||||
|
||||
data EncodedChatMessage = ECMEncoded ByteString | ECMLarge
|
||||
|
||||
@@ -1384,8 +1390,8 @@ appBinaryToCM AppMessageBinary {msgId, tag, body} = do
|
||||
msg = \case
|
||||
BFileChunk_ -> BFileChunk <$> (SharedMsgId <$> smpP) <*> (unIFC <$> smpP)
|
||||
|
||||
appJsonToCM :: AppMessageJson -> Either String (ChatMessage 'Json)
|
||||
appJsonToCM AppMessageJson {v, msgId, event, params} = do
|
||||
appJsonToCM :: Int -> AppMessageJson -> Either String (ChatMessage 'Json)
|
||||
appJsonToCM fwdDepth AppMessageJson {v, msgId, event, params} = do
|
||||
eventTag <- strDecode $ encodeUtf8 event
|
||||
chatMsgEvent <- msg eventTag
|
||||
pure ChatMessage {chatVRange = maybe chatInitialVRange fromChatVRange v, msgId, chatMsgEvent}
|
||||
@@ -1460,11 +1466,12 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do
|
||||
XGrpRosterAck_ -> XGrpRosterAck <$> p "version" <*> opt "error"
|
||||
XGrpRosterRequest_ -> XGrpRosterRequest <$> opt "version"
|
||||
XGrpMsgForward_ -> do
|
||||
when (fwdDepth >= maxFwdDepth) $ Left "forward depth exceeds limit"
|
||||
fwdSender <- opt "memberId" >>= \case
|
||||
Just memberId -> FwdMember memberId . fromMaybe "" <$> opt "memberName"
|
||||
Nothing -> pure FwdChannel
|
||||
fwdBrokerTs <- p "msgTs"
|
||||
XGrpMsgForward (GrpMsgForward {fwdSender, fwdBrokerTs}) <$> p "msg"
|
||||
XGrpMsgForward (GrpMsgForward {fwdSender, fwdBrokerTs}) <$> (appJsonToCM (fwdDepth + 1) =<< p "msg")
|
||||
XInfoProbe_ -> XInfoProbe <$> p "probe"
|
||||
XInfoProbeCheck_ -> XInfoProbeCheck <$> p "probeHash"
|
||||
XInfoProbeOk_ -> XInfoProbeOk <$> p "probe"
|
||||
@@ -1577,7 +1584,7 @@ instance ToJSON (ChatMessage 'Json) where
|
||||
toJSON = (\(AMJson msg) -> toJSON msg) . chatToAppMessage
|
||||
|
||||
instance FromJSON (ChatMessage 'Json) where
|
||||
parseJSON v = appJsonToCM <$?> parseJSON v
|
||||
parseJSON v = appJsonToCM 0 <$?> parseJSON v
|
||||
|
||||
instance FromField (ChatMessage 'Json) where
|
||||
fromField = blobFieldDecoder J.eitherDecodeStrict'
|
||||
|
||||
@@ -75,11 +75,11 @@ remoteFilesFolder = "simplex_v1_files"
|
||||
|
||||
-- when acting as host
|
||||
minRemoteCtrlVersion :: AppVersion
|
||||
minRemoteCtrlVersion = AppVersion [7, 1, 0, 5]
|
||||
minRemoteCtrlVersion = AppVersion [7, 1, 0, 9]
|
||||
|
||||
-- when acting as controller
|
||||
minRemoteHostVersion :: AppVersion
|
||||
minRemoteHostVersion = AppVersion [7, 1, 0, 5]
|
||||
minRemoteHostVersion = AppVersion [7, 1, 0, 9]
|
||||
|
||||
currentAppVersion :: AppVersion
|
||||
currentAppVersion = AppVersion SC.version
|
||||
@@ -517,7 +517,7 @@ handleRemoteCommand execCC encryption remoteOutputQ HTTP2Request {request, reqBo
|
||||
where
|
||||
parseRequest :: ExceptT RemoteProtocolError IO (C.SbKeyNonce, GetChunk, RemoteCommand)
|
||||
parseRequest = do
|
||||
(rfKN, header, getNext) <- parseDecryptHTTP2Body encryption request reqBody
|
||||
(rfKN, header, getNext) <- parseDecryptHTTP2Body maxCommandBodySize encryption request reqBody
|
||||
(rfKN,getNext,) <$> liftEitherWith RPEInvalidJSON (J.eitherDecodeStrict header)
|
||||
replyError = reply . RRChatResponse . RRError
|
||||
processCommand :: User -> C.SbKeyNonce -> GetChunk -> RemoteCommand -> CM ()
|
||||
|
||||
@@ -40,7 +40,8 @@ import Simplex.Chat.Controller
|
||||
import Simplex.Chat.Remote.Transport
|
||||
import Simplex.Chat.Remote.Types
|
||||
import Simplex.Chat.Types (BoolDef (..))
|
||||
import Simplex.FileTransfer.Description (FileDigest (..))
|
||||
import Simplex.FileTransfer.Description (FileDigest (..), mb)
|
||||
import Simplex.Messaging.Compression (limitDecompress')
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.File (CryptoFile (..))
|
||||
import Simplex.Messaging.Crypto.Lazy (LazyByteString)
|
||||
@@ -180,7 +181,7 @@ sendRemoteCommand RemoteHostClient {httpClient, hostEncoding, encryption} file_
|
||||
encFile_ <- mapM (prepareEncryptedFile sfKN) file_
|
||||
let req = httpRequest encFile_ encCmd
|
||||
HTTP2Response {response, respBody} <- liftError' (RPEHTTP2 . tshow) $ sendRequestDirect httpClient req Nothing
|
||||
(rfKN, header, getNext) <- parseDecryptHTTP2Body encryption response respBody
|
||||
(rfKN, header, getNext) <- parseDecryptHTTP2Body maxResponseBodySize encryption response respBody
|
||||
rr <- liftEitherWith (RPEInvalidJSON . fromString) $ J.eitherDecodeStrict header >>= JT.parseEither J.parseJSON . convertJSON hostEncoding localEncoding
|
||||
pure (rfKN, getNext, rr)
|
||||
where
|
||||
@@ -273,9 +274,15 @@ encryptEncodeHTTP2Body corrId cmdKN RemoteCrypto {sessionCode, signatures, compr
|
||||
sign :: C.PrivateKeyEd25519 -> CH.Context SHA512 -> ByteString
|
||||
sign k = C.signatureBytes . C.sign' k . BA.convert . CH.hashFinalize
|
||||
|
||||
maxCommandBodySize :: Int
|
||||
maxCommandBodySize = mb 64
|
||||
|
||||
maxResponseBodySize :: Int
|
||||
maxResponseBodySize = mb 512
|
||||
|
||||
-- | Parse and decrypt HTTP2 request/response
|
||||
parseDecryptHTTP2Body :: HTTP2BodyChunk a => RemoteCrypto -> a -> HTTP2Body -> ExceptT RemoteProtocolError IO (C.SbKeyNonce, ByteString, Int -> IO ByteString)
|
||||
parseDecryptHTTP2Body rc@RemoteCrypto {sessionCode, signatures, compression} hr HTTP2Body {bodyBuffer} = do
|
||||
parseDecryptHTTP2Body :: HTTP2BodyChunk a => Int -> RemoteCrypto -> a -> HTTP2Body -> ExceptT RemoteProtocolError IO (C.SbKeyNonce, ByteString, Int -> IO ByteString)
|
||||
parseDecryptHTTP2Body maxSize rc@RemoteCrypto {sessionCode, signatures, compression} hr HTTP2Body {bodyBuffer} = do
|
||||
(corrId, ct) <- getBody
|
||||
(cmdKN, rfKN) <- ExceptT $ atomically $ getRemoteRcvKeys rc corrId
|
||||
s <- liftError PRERemoteControl $ RC.rcDecryptBody cmdKN ct
|
||||
@@ -287,7 +294,7 @@ parseDecryptHTTP2Body rc@RemoteCrypto {sessionCode, signatures, compression} hr
|
||||
corrIdStr <- liftIO $ getNext 4
|
||||
ctLenStr <- liftIO $ getNext 4
|
||||
let ctLen = decodeWord32 ctLenStr
|
||||
when (ctLen > fromIntegral (maxBound :: Int)) $ throwError RPEInvalidSize
|
||||
when (ctLen > fromIntegral maxSize) $ throwError RPEInvalidSize
|
||||
chunks <- liftIO $ getLazy $ fromIntegral ctLen
|
||||
let hc = CH.hashUpdates (CH.hashInit @SHA512) [corrIdStr, ctLenStr]
|
||||
hc' = CH.hashUpdates hc chunks
|
||||
@@ -330,8 +337,5 @@ parseDecryptHTTP2Body rc@RemoteCrypto {sessionCode, signatures, compression} hr
|
||||
getNext sz = getBuffered bodyBuffer sz Nothing $ getBodyChunk hr
|
||||
decompress :: LazyByteString -> ExceptT RemoteProtocolError IO ByteString
|
||||
decompress s
|
||||
| compression = case Z1.decompress $ LB.toStrict s of
|
||||
Z1.Error e -> throwError $ RPEInvalidBody e
|
||||
Z1.Skip -> pure B.empty
|
||||
Z1.Decompress s' -> pure s'
|
||||
| compression = liftEitherWith RPEInvalidBody $ limitDecompress' maxSize $ LB.toStrict s
|
||||
| otherwise = pure $ LB.toStrict s
|
||||
|
||||
@@ -18,6 +18,7 @@ import Control.Monad (when)
|
||||
import qualified Data.Aeson.TH as J
|
||||
import Data.ByteString (ByteString)
|
||||
import Data.Int (Int64)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Text (Text)
|
||||
import Data.Word (Word16, Word32)
|
||||
import Simplex.Chat.Remote.AppVersion
|
||||
@@ -73,8 +74,10 @@ getRemoteRcvKeys RemoteCrypto {rcvCounter, chainKeys = TSbChainKeys {rcvKey}, sk
|
||||
| otherwise = do -- prevCorrId < corrId
|
||||
writeTVar rcvCounter corrId
|
||||
skipKeys (prevCorrId + 1)
|
||||
modifyTVar' skippedKeys $ \m -> M.drop (M.size m - maxSkippedKeys) m
|
||||
Right <$> getKeys
|
||||
maxSkip = 256
|
||||
maxSkippedKeys = 1024
|
||||
getKeys = (,) <$> stateTVar rcvKey C.sbcHkdf <*> stateTVar rcvKey C.sbcHkdf
|
||||
skipKeys !cId =
|
||||
when (cId < corrId) $ do
|
||||
|
||||
@@ -1169,6 +1169,6 @@ getDirectChatTTL db ctId =
|
||||
|
||||
getUserContactsToExpire :: DB.Connection -> User -> Int64 -> IO [ContactId]
|
||||
getUserContactsToExpire db User {userId} globalTTL =
|
||||
map fromOnly <$> DB.query db ("SELECT contact_id FROM contacts WHERE user_id = ? AND chat_item_ttl > 0" <> cond) (Only userId)
|
||||
map fromOnly <$> DB.query db ("SELECT contact_id FROM contacts WHERE user_id = ? AND (chat_item_ttl > 0" <> cond <> ")") (Only userId)
|
||||
where
|
||||
cond = if globalTTL == 0 then "" else " OR chat_item_ttl IS NULL"
|
||||
|
||||
@@ -3105,10 +3105,10 @@ associateContactWithMemberRecord
|
||||
db
|
||||
[sql|
|
||||
UPDATE group_members
|
||||
SET contact_id = ?, updated_at = ?
|
||||
WHERE user_id = ? AND group_id = ? AND group_member_id = ?
|
||||
SET contact_id = ?, local_display_name = ?, contact_profile_id = ?, updated_at = ?
|
||||
WHERE (user_id = ? AND group_id = ? AND group_member_id = ?) OR contact_id = ?
|
||||
|]
|
||||
(contactId, currentTs, userId, groupId, groupMemberId)
|
||||
(contactId, memLDN, memProfileId, currentTs, userId, groupId, groupMemberId, contactId)
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
@@ -3412,7 +3412,16 @@ setMemberContactStartedConnection db Contact {contactId} = do
|
||||
(BI True, currentTs, contactId)
|
||||
|
||||
updateMemberProfile :: DB.Connection -> StoreCxt -> User -> GroupMember -> Profile -> ExceptT StoreError IO GroupMember
|
||||
updateMemberProfile db cxt user@User {userId} m p' = do
|
||||
updateMemberProfile db cxt user m@GroupMember {memberContactId} p' = case memberContactId of
|
||||
Nothing -> updateUnlinkedMemberProfile db cxt user m p'
|
||||
Just ctId -> do
|
||||
ct <- getContact db cxt user ctId
|
||||
if contactUpdatableFromMember ct
|
||||
then fst <$> updateContactMemberProfile db cxt user m ct p'
|
||||
else pure m
|
||||
|
||||
updateUnlinkedMemberProfile :: DB.Connection -> StoreCxt -> User -> GroupMember -> Profile -> ExceptT StoreError IO GroupMember
|
||||
updateUnlinkedMemberProfile db cxt user@User {userId} m p' = do
|
||||
currentTs <- liftIO getCurrentTime
|
||||
badgeVerified <- liftIO $ profileBadgeVerified (badgeKeys cxt) (memberProfile m) p'
|
||||
let memberProfile = toLocalProfile profileId p' localAlias currentTs badgeVerified Nothing
|
||||
@@ -3494,8 +3503,7 @@ createNewUnknownGroupMember db cxt user@User {userId, userContactId} GroupInfo {
|
||||
createLinkOwnerMember :: DB.Connection -> StoreCxt -> User -> GroupInfo -> Maybe ContactId -> MemberId -> C.PublicKeyEd25519 -> ExceptT StoreError IO GroupMember
|
||||
createLinkOwnerMember db cxt user@User {userId, userContactId} GroupInfo {groupId} contactId_ memberId ownerKey = do
|
||||
currentTs <- liftIO getCurrentTime
|
||||
let memberProfile = profileFromName $ nameFromMemberId memberId
|
||||
(localDisplayName, profileId, _) <- createNewMemberProfile_ db cxt user memberProfile currentTs
|
||||
(localDisplayName, profileId) <- maybe (newOwnerProfile currentTs) contactNameAndProfile contactId_
|
||||
indexInGroup <- getUpdateNextIndexInGroup_ db groupId
|
||||
liftIO $
|
||||
DB.execute
|
||||
@@ -3515,6 +3523,12 @@ createLinkOwnerMember db cxt user@User {userId, userContactId} GroupInfo {groupI
|
||||
getGroupMemberById db cxt user groupMemberId
|
||||
where
|
||||
VersionRange minV maxV = vr cxt
|
||||
newOwnerProfile currentTs = do
|
||||
(ldn, pId, _) <- createNewMemberProfile_ db cxt user (profileFromName $ nameFromMemberId memberId) currentTs
|
||||
pure (ldn, pId)
|
||||
contactNameAndProfile ctId = do
|
||||
Contact {localDisplayName = ldn, profile = LocalProfile {profileId = pId}} <- getContact db cxt user ctId
|
||||
pure (ldn, pId)
|
||||
|
||||
-- Intro refreshes only profile / status / peer version. Role and key stay owner-authoritative
|
||||
-- (the owner-signed roster for members/moderators/admins, link data for owners), so taking either from
|
||||
@@ -3650,7 +3664,7 @@ getGroupChatTTL db gId =
|
||||
|
||||
getUserGroupsToExpire :: DB.Connection -> User -> Int64 -> IO [GroupId]
|
||||
getUserGroupsToExpire db User {userId} globalTTL =
|
||||
map fromOnly <$> DB.query db ("SELECT group_id FROM groups WHERE user_id = ? AND chat_item_ttl > 0" <> cond) (Only userId)
|
||||
map fromOnly <$> DB.query db ("SELECT group_id FROM groups WHERE user_id = ? AND (chat_item_ttl > 0" <> cond <> ")") (Only userId)
|
||||
where
|
||||
cond = if globalTTL == 0 then "" else " OR chat_item_ttl IS NULL"
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ module Simplex.Chat.Store.Profiles
|
||||
createCall,
|
||||
deleteCalls,
|
||||
getCalls,
|
||||
expireCalls,
|
||||
createCommand,
|
||||
setCommandConnId,
|
||||
deleteCommand,
|
||||
@@ -103,6 +104,7 @@ import Data.Time.Clock (UTCTime (..), getCurrentTime)
|
||||
import Simplex.Chat.Badges (LocalBadge, localBadgeToRow)
|
||||
import Simplex.Chat.Call
|
||||
import Simplex.Chat.Messages
|
||||
import Simplex.Chat.Messages.CIContent
|
||||
import Simplex.Chat.Operators
|
||||
import Simplex.Chat.Protocol
|
||||
import Simplex.Chat.Store.Direct
|
||||
@@ -126,7 +128,7 @@ import Simplex.Messaging.Agent.Store.Entity
|
||||
import Simplex.Messaging.Transport.Client (TransportHost)
|
||||
import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8)
|
||||
#if defined(dbPostgres)
|
||||
import Database.PostgreSQL.Simple (Only (..), Query, (:.) (..))
|
||||
import Database.PostgreSQL.Simple (In (..), Only (..), Query, (:.) (..))
|
||||
import Database.PostgreSQL.Simple.SqlQQ (sql)
|
||||
#else
|
||||
import Database.SQLite.Simple (Only (..), Query, (:.) (..))
|
||||
@@ -1084,6 +1086,26 @@ getCalls db =
|
||||
toCall :: (ContactId, CallId, Text, ChatItemId, CallState, UTCTime) -> Call
|
||||
toCall (contactId, callId, callUUID, chatItemId, callState, callTs) = Call {contactId, callId, callUUID, chatItemId, callState, callTs}
|
||||
|
||||
-- only received call invitations are stored, so their chat items are pending and become missed
|
||||
expireCalls :: DB.Connection -> UTCTime -> IO ()
|
||||
expireCalls db cutoffTs = do
|
||||
itemIds :: [ChatItemId] <- map fromOnly <$> DB.query db "DELETE FROM calls WHERE call_ts < ? RETURNING chat_item_id" (Only cutoffTs)
|
||||
currentTs <- getCurrentTime
|
||||
let content = CIRcvCall CISCallMissed 0
|
||||
contentText = ciContentToText content
|
||||
unless (null itemIds) $
|
||||
#if defined(dbPostgres)
|
||||
DB.execute
|
||||
db
|
||||
"UPDATE chat_items SET item_content = ?, item_text = ?, updated_at = ? WHERE chat_item_id IN ?"
|
||||
(content, contentText, currentTs, In itemIds)
|
||||
#else
|
||||
DB.executeMany
|
||||
db
|
||||
"UPDATE chat_items SET item_content = ?, item_text = ?, updated_at = ? WHERE chat_item_id = ?"
|
||||
(map (content,contentText,currentTs,) itemIds)
|
||||
#endif
|
||||
|
||||
createCommand :: DB.Connection -> User -> Maybe Int64 -> CommandFunction -> IO CommandId
|
||||
createCommand db User {userId} connId commandFunction = do
|
||||
currentTs <- getCurrentTime
|
||||
|
||||
@@ -144,7 +144,7 @@ SEARCH r USING INDEX idx_rcv_file_chunk_replicas_rcv_file_chunk_id (rcv_file_chu
|
||||
SEARCH s USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query:
|
||||
SELECT c.corr_id, cs.user_id, c.command
|
||||
SELECT c.corr_id, cs.user_id, c.command, c.created_at
|
||||
FROM commands c
|
||||
JOIN connections cs USING (conn_id)
|
||||
WHERE c.command_id = ?
|
||||
@@ -249,6 +249,26 @@ Plan:
|
||||
SEARCH snd_files USING INDEX idx_snd_files_status_created_at (status=? AND created_at>?)
|
||||
USE TEMP B-TREE FOR ORDER BY
|
||||
|
||||
Query:
|
||||
SELECT DISTINCT
|
||||
s.xftp_host, s.xftp_port, s.xftp_key_hash
|
||||
FROM rcv_file_chunk_replicas r
|
||||
JOIN xftp_servers s ON s.xftp_server_id = r.xftp_server_id
|
||||
JOIN rcv_file_chunks c ON c.rcv_file_chunk_id = r.rcv_file_chunk_id
|
||||
JOIN rcv_files f ON f.rcv_file_id = c.rcv_file_id
|
||||
WHERE (f.rcv_file_id = ? OR f.redirect_id = ?) AND r.replica_number = 1
|
||||
|
||||
Plan:
|
||||
MULTI-INDEX OR
|
||||
INDEX 1
|
||||
SEARCH f USING INTEGER PRIMARY KEY (rowid=?)
|
||||
INDEX 2
|
||||
SEARCH f USING COVERING INDEX idx_rcv_files_redirect_id (redirect_id=?)
|
||||
SEARCH c USING COVERING INDEX idx_rcv_file_chunks_rcv_file_id (rcv_file_id=?)
|
||||
SEARCH r USING INDEX idx_rcv_file_chunk_replicas_rcv_file_chunk_id (rcv_file_chunk_id=?)
|
||||
SEARCH s USING INTEGER PRIMARY KEY (rowid=?)
|
||||
USE TEMP B-TREE FOR DISTINCT
|
||||
|
||||
Query:
|
||||
SELECT MAX(internal_id)
|
||||
FROM messages
|
||||
@@ -273,6 +293,16 @@ Query:
|
||||
Plan:
|
||||
SEARCH connections USING PRIMARY KEY (conn_id=?)
|
||||
|
||||
Query:
|
||||
SELECT user_id FROM users u
|
||||
WHERE u.deleted = ?
|
||||
AND NOT EXISTS (SELECT c.conn_id FROM connections c WHERE c.user_id = u.user_id)
|
||||
|
||||
Plan:
|
||||
SCAN u
|
||||
CORRELATED SCALAR SUBQUERY 1
|
||||
SEARCH c USING COVERING INDEX idx_connections_user (user_id=?)
|
||||
|
||||
Query:
|
||||
SELECT user_id FROM users u
|
||||
WHERE u.user_id = ?
|
||||
@@ -495,6 +525,23 @@ Query:
|
||||
Plan:
|
||||
SEARCH conn_confirmations USING INDEX idx_conn_confirmations_conn_id (conn_id=?)
|
||||
|
||||
Query:
|
||||
SELECT conn_id, (
|
||||
SELECT processed_ratchet_key_hash_id
|
||||
FROM processed_ratchet_key_hashes
|
||||
WHERE conn_id = c.conn_id
|
||||
ORDER BY processed_ratchet_key_hash_id DESC
|
||||
LIMIT 1 OFFSET ?
|
||||
) AS max_excess_id
|
||||
FROM processed_ratchet_key_hashes c
|
||||
GROUP BY conn_id
|
||||
HAVING COUNT(*) > ?
|
||||
|
||||
Plan:
|
||||
SCAN c USING COVERING INDEX idx_processed_ratchet_key_hashes_conn_id
|
||||
CORRELATED SCALAR SUBQUERY 1
|
||||
SEARCH processed_ratchet_key_hashes USING COVERING INDEX idx_processed_ratchet_key_hashes_conn_id (conn_id=?)
|
||||
|
||||
Query:
|
||||
SELECT conn_id, ratchet_state, sender_key, e2e_snd_pub_key, sender_conn_info, smp_reply_queues, smp_client_version
|
||||
FROM conn_confirmations
|
||||
@@ -567,6 +614,15 @@ Query:
|
||||
Plan:
|
||||
SEARCH address_ratchet_keys USING INDEX idx_address_ratchet_keys (conn_id=? AND ratchet_key_id=?)
|
||||
|
||||
Query:
|
||||
UPDATE rcv_messages
|
||||
SET receive_attempts = receive_attempts + 1
|
||||
WHERE conn_id = ? AND internal_id = ?
|
||||
RETURNING receive_attempts
|
||||
|
||||
Plan:
|
||||
SEARCH rcv_messages USING COVERING INDEX idx_rcv_messages_conn_id_internal_id (conn_id=? AND internal_id=?)
|
||||
|
||||
Query:
|
||||
DELETE FROM address_ratchet_keys
|
||||
WHERE conn_id = ?
|
||||
@@ -591,6 +647,39 @@ Query:
|
||||
Plan:
|
||||
SEARCH conn_confirmations USING COVERING INDEX idx_conn_confirmations_conn_id (conn_id=?)
|
||||
|
||||
Query:
|
||||
DELETE FROM encrypted_rcv_message_hashes
|
||||
WHERE encrypted_rcv_message_hash_id IN (
|
||||
SELECT encrypted_rcv_message_hash_id
|
||||
FROM encrypted_rcv_message_hashes
|
||||
WHERE created_at < ?
|
||||
ORDER BY created_at ASC
|
||||
LIMIT ?
|
||||
)
|
||||
|
||||
Plan:
|
||||
SEARCH encrypted_rcv_message_hashes USING INTEGER PRIMARY KEY (rowid=?)
|
||||
LIST SUBQUERY 1
|
||||
SEARCH encrypted_rcv_message_hashes USING COVERING INDEX idx_encrypted_rcv_message_hashes_created_at (created_at<?)
|
||||
|
||||
Query:
|
||||
DELETE FROM messages
|
||||
WHERE (conn_id, internal_id) IN (
|
||||
SELECT conn_id, internal_id
|
||||
FROM messages
|
||||
WHERE internal_ts < ? AND internal_snd_id IS NOT NULL
|
||||
ORDER BY internal_ts ASC
|
||||
LIMIT ?
|
||||
)
|
||||
|
||||
Plan:
|
||||
SEARCH messages USING COVERING INDEX idx_messages_conn_id (conn_id=? AND internal_id=?)
|
||||
LIST SUBQUERY 1
|
||||
SEARCH messages USING INDEX idx_messages_internal_ts (internal_ts<?)
|
||||
SEARCH snd_message_deliveries USING COVERING INDEX idx_snd_message_deliveries_conn_id_internal_id (conn_id=? AND internal_id=?)
|
||||
SEARCH snd_messages USING COVERING INDEX idx_snd_messages_conn_id_internal_id (conn_id=? AND internal_id=?)
|
||||
SEARCH rcv_messages USING COVERING INDEX idx_rcv_messages_conn_id_internal_id (conn_id=? AND internal_id=?)
|
||||
|
||||
Query:
|
||||
INSERT INTO address_ratchet_keys
|
||||
(conn_id, ratchet_key_id, x3dh_priv_key_1, x3dh_priv_key_2, pq_priv_kem)
|
||||
@@ -736,6 +825,30 @@ Query:
|
||||
|
||||
Plan:
|
||||
|
||||
Query:
|
||||
SELECT rcv_file_id, rcv_file_entity_id, prefix_path
|
||||
FROM rcv_files
|
||||
WHERE created_at < ?
|
||||
|
||||
Plan:
|
||||
SCAN rcv_files
|
||||
|
||||
Query:
|
||||
SELECT rcv_file_id, rcv_file_entity_id, prefix_path
|
||||
FROM rcv_files
|
||||
WHERE deleted = 1
|
||||
|
||||
Plan:
|
||||
SCAN rcv_files
|
||||
|
||||
Query:
|
||||
SELECT rcv_file_id, rcv_file_entity_id, tmp_path
|
||||
FROM rcv_files
|
||||
WHERE status IN (?,?) AND tmp_path IS NOT NULL
|
||||
|
||||
Plan:
|
||||
SEARCH rcv_files USING INDEX idx_rcv_files_status_created_at (status=?)
|
||||
|
||||
Query:
|
||||
SELECT rcv_replica_id, rcv_replica_key
|
||||
FROM snd_file_chunk_replica_recipients
|
||||
@@ -744,6 +857,30 @@ Query:
|
||||
Plan:
|
||||
SEARCH snd_file_chunk_replica_recipients USING INDEX idx_snd_file_chunk_replica_recipients_snd_file_chunk_replica_id (snd_file_chunk_replica_id=?)
|
||||
|
||||
Query:
|
||||
SELECT snd_file_id, snd_file_entity_id, prefix_path
|
||||
FROM snd_files
|
||||
WHERE created_at < ?
|
||||
|
||||
Plan:
|
||||
SCAN snd_files
|
||||
|
||||
Query:
|
||||
SELECT snd_file_id, snd_file_entity_id, prefix_path
|
||||
FROM snd_files
|
||||
WHERE deleted = 1
|
||||
|
||||
Plan:
|
||||
SCAN snd_files
|
||||
|
||||
Query:
|
||||
SELECT snd_file_id, snd_file_entity_id, prefix_path
|
||||
FROM snd_files
|
||||
WHERE status IN (?,?) AND prefix_path IS NOT NULL
|
||||
|
||||
Plan:
|
||||
SEARCH snd_files USING INDEX idx_snd_files_status_created_at (status=?)
|
||||
|
||||
Query:
|
||||
UPDATE client_services
|
||||
SET service_id = ?
|
||||
@@ -1001,7 +1138,7 @@ SEARCH s USING PRIMARY KEY (host=? AND port=?)
|
||||
SEARCH c USING PRIMARY KEY (conn_id=?)
|
||||
|
||||
Query:
|
||||
SELECT c.user_id, q.conn_id, q.host, q.port, COALESCE(q.server_key_hash, s.key_hash), q.rcv_id, q.rcv_private_key, q.status, c.enable_ntfs, q.client_notice_id,
|
||||
SELECT c.user_id, q.conn_id, q.rcv_id, q.rcv_private_key, q.status, c.enable_ntfs, q.client_notice_id,
|
||||
q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id
|
||||
FROM rcv_queues q
|
||||
JOIN servers s ON q.host = s.host AND q.port = s.port
|
||||
@@ -1013,7 +1150,7 @@ SEARCH q USING PRIMARY KEY (host=? AND port=?)
|
||||
SEARCH c USING PRIMARY KEY (conn_id=?)
|
||||
|
||||
Query:
|
||||
SELECT c.user_id, q.conn_id, q.host, q.port, COALESCE(q.server_key_hash, s.key_hash), q.rcv_id, q.rcv_private_key, q.status, c.enable_ntfs, q.client_notice_id,
|
||||
SELECT c.user_id, q.conn_id, q.rcv_id, q.rcv_private_key, q.status, c.enable_ntfs, q.client_notice_id,
|
||||
q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id
|
||||
FROM rcv_queues q
|
||||
JOIN servers s ON q.host = s.host AND q.port = s.port
|
||||
@@ -1025,7 +1162,7 @@ SEARCH q USING PRIMARY KEY (host=? AND port=?)
|
||||
SEARCH c USING PRIMARY KEY (conn_id=?)
|
||||
|
||||
Query:
|
||||
SELECT c.user_id, q.conn_id, q.host, q.port, COALESCE(q.server_key_hash, s.key_hash), q.rcv_id, q.rcv_private_key, q.status, c.enable_ntfs, q.client_notice_id,
|
||||
SELECT c.user_id, q.conn_id, q.rcv_id, q.rcv_private_key, q.status, c.enable_ntfs, q.client_notice_id,
|
||||
q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id
|
||||
FROM rcv_queues q
|
||||
JOIN servers s ON q.host = s.host AND q.port = s.port
|
||||
@@ -1048,11 +1185,15 @@ Query: DELETE FROM conn_invitations WHERE invitation_id = ?
|
||||
Plan:
|
||||
SEARCH conn_invitations USING PRIMARY KEY (invitation_id=?)
|
||||
|
||||
Query: DELETE FROM conn_invitations WHERE service_request = 1 AND created_at < ?
|
||||
Plan:
|
||||
SCAN conn_invitations
|
||||
|
||||
Query: DELETE FROM connections WHERE conn_id = ?
|
||||
Plan:
|
||||
SEARCH connections USING PRIMARY KEY (conn_id=?)
|
||||
SEARCH address_ratchet_keys USING COVERING INDEX idx_address_ratchet_keys (conn_id=?)
|
||||
SEARCH processed_ratchet_key_hashes USING COVERING INDEX idx_processed_ratchet_key_hashes_hash (conn_id=?)
|
||||
SEARCH processed_ratchet_key_hashes USING COVERING INDEX idx_processed_ratchet_key_hashes_conn_id (conn_id=?)
|
||||
SEARCH encrypted_rcv_message_hashes USING COVERING INDEX idx_encrypted_rcv_message_hashes_hash (conn_id=?)
|
||||
SEARCH snd_message_deliveries USING COVERING INDEX idx_snd_message_deliveries_conn_id_internal_id (conn_id=?)
|
||||
SEARCH commands USING COVERING INDEX idx_commands_conn_id (conn_id=?)
|
||||
@@ -1067,7 +1208,7 @@ Query: DELETE FROM connections WHERE user_id = 2
|
||||
Plan:
|
||||
SEARCH connections USING COVERING INDEX idx_connections_user (user_id=?)
|
||||
SEARCH address_ratchet_keys USING COVERING INDEX idx_address_ratchet_keys (conn_id=?)
|
||||
SEARCH processed_ratchet_key_hashes USING COVERING INDEX idx_processed_ratchet_key_hashes_hash (conn_id=?)
|
||||
SEARCH processed_ratchet_key_hashes USING COVERING INDEX idx_processed_ratchet_key_hashes_conn_id (conn_id=?)
|
||||
SEARCH encrypted_rcv_message_hashes USING COVERING INDEX idx_encrypted_rcv_message_hashes_hash (conn_id=?)
|
||||
SEARCH snd_message_deliveries USING COVERING INDEX idx_snd_message_deliveries_conn_id_internal_id (conn_id=?)
|
||||
SEARCH commands USING COVERING INDEX idx_commands_conn_id (conn_id=?)
|
||||
@@ -1078,6 +1219,10 @@ SEARCH messages USING COVERING INDEX idx_messages_conn_id (conn_id=?)
|
||||
SEARCH snd_queues USING COVERING INDEX idx_snd_queue_id (conn_id=?)
|
||||
SEARCH rcv_queues USING COVERING INDEX idx_rcv_queue_id (conn_id=?)
|
||||
|
||||
Query: DELETE FROM deleted_snd_chunk_replicas WHERE created_at < ?
|
||||
Plan:
|
||||
SEARCH deleted_snd_chunk_replicas USING COVERING INDEX idx_deleted_snd_chunk_replicas_pending (created_at<?)
|
||||
|
||||
Query: DELETE FROM deleted_snd_chunk_replicas WHERE deleted_snd_chunk_replica_id = ?
|
||||
Plan:
|
||||
SEARCH deleted_snd_chunk_replicas USING INTEGER PRIMARY KEY (rowid=?)
|
||||
@@ -1097,6 +1242,14 @@ Query: DELETE FROM ntf_subscriptions WHERE conn_id = ?
|
||||
Plan:
|
||||
SEARCH ntf_subscriptions USING PRIMARY KEY (conn_id=?)
|
||||
|
||||
Query: DELETE FROM ntf_tokens_to_delete WHERE created_at < ?
|
||||
Plan:
|
||||
SCAN ntf_tokens_to_delete
|
||||
|
||||
Query: DELETE FROM processed_ratchet_key_hashes WHERE conn_id = ? AND processed_ratchet_key_hash_id <= ? AND created_at < ?
|
||||
Plan:
|
||||
SEARCH processed_ratchet_key_hashes USING INDEX idx_processed_ratchet_key_hashes_conn_id (conn_id=? AND processed_ratchet_key_hash_id<?)
|
||||
|
||||
Query: DELETE FROM ratchets WHERE conn_id = ?
|
||||
Plan:
|
||||
SEARCH ratchets USING PRIMARY KEY (conn_id=?)
|
||||
@@ -1222,6 +1375,18 @@ Query: SELECT conn_id FROM connections WHERE deleted = 0
|
||||
Plan:
|
||||
SEARCH connections USING COVERING INDEX idx_connections_deleted (deleted=?)
|
||||
|
||||
Query: SELECT conn_id FROM connections WHERE deleted = ?
|
||||
Plan:
|
||||
SEARCH connections USING COVERING INDEX idx_connections_deleted (deleted=?)
|
||||
|
||||
Query: SELECT conn_id FROM connections WHERE deleted_at_wait_delivery IS NOT NULL
|
||||
Plan:
|
||||
SCAN connections
|
||||
|
||||
Query: SELECT conn_id FROM connections WHERE service_request_expires_at < ? AND deleted = 0 AND deleted_at_wait_delivery IS NULL
|
||||
Plan:
|
||||
SEARCH connections USING INDEX idx_connections_deleted (deleted=?)
|
||||
|
||||
Query: SELECT conn_id FROM connections WHERE user_id = ?
|
||||
Plan:
|
||||
SEARCH connections USING COVERING INDEX idx_connections_user (user_id=?)
|
||||
@@ -1230,6 +1395,10 @@ Query: SELECT conn_id, rc_verify_code_ad, rc_verify_code_pq, CASE WHEN rc_verify
|
||||
Plan:
|
||||
SEARCH ratchets USING PRIMARY KEY (conn_id=?)
|
||||
|
||||
Query: SELECT count(1) FROM commands
|
||||
Plan:
|
||||
SCAN commands USING COVERING INDEX idx_commands_conn_id
|
||||
|
||||
Query: SELECT count(1) FROM connections
|
||||
Plan:
|
||||
SCAN connections USING COVERING INDEX idx_connections_deleted
|
||||
@@ -1250,10 +1419,6 @@ Query: SELECT deleted FROM snd_files WHERE snd_file_id = ?
|
||||
Plan:
|
||||
SEARCH snd_files USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query: SELECT header_key, msg_n, msg_key FROM skipped_messages WHERE conn_id = ?
|
||||
Plan:
|
||||
SEARCH skipped_messages USING INDEX idx_skipped_messages_conn_id (conn_id=?)
|
||||
|
||||
Query: SELECT key_hash FROM servers WHERE host = ? AND port = ?
|
||||
Plan:
|
||||
SEARCH servers USING PRIMARY KEY (host=? AND port=?)
|
||||
@@ -1270,6 +1435,10 @@ Query: SELECT ratchet_state, x3dh_pub_key_1, x3dh_pub_key_2, pq_pub_kem FROM rat
|
||||
Plan:
|
||||
SEARCH ratchets USING PRIMARY KEY (conn_id=?)
|
||||
|
||||
Query: SELECT ratchet_sync_state FROM connections WHERE conn_id = ?
|
||||
Plan:
|
||||
SEARCH connections USING PRIMARY KEY (conn_id=?)
|
||||
|
||||
Query: SELECT rcv_file_id FROM rcv_files WHERE rcv_file_entity_id = ?
|
||||
Plan:
|
||||
SEARCH rcv_files USING COVERING INDEX sqlite_autoindex_rcv_files_1 (rcv_file_entity_id=?)
|
||||
@@ -1290,6 +1459,10 @@ Query: SELECT rcv_queue_id, broker_id FROM rcv_messages WHERE conn_id = ? AND in
|
||||
Plan:
|
||||
SEARCH rcv_messages USING PRIMARY KEY (conn_id=?)
|
||||
|
||||
Query: SELECT skipped_message_id, header_key, msg_n, msg_key FROM skipped_messages WHERE conn_id = ? ORDER BY skipped_message_id DESC LIMIT ?
|
||||
Plan:
|
||||
SEARCH skipped_messages USING INDEX idx_skipped_messages_conn_id (conn_id=?)
|
||||
|
||||
Query: SELECT snd_file_id FROM snd_files WHERE snd_file_entity_id = ?
|
||||
Plan:
|
||||
SEARCH snd_files USING COVERING INDEX idx_snd_files_snd_file_entity_id (snd_file_entity_id=?)
|
||||
@@ -1374,6 +1547,10 @@ Query: UPDATE rcv_files SET key = ?, nonce = ?, chunk_size = ?, updated_at = ? W
|
||||
Plan:
|
||||
SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query: UPDATE rcv_files SET status = ?, updated_at = ? WHERE (rcv_file_id = ? OR redirect_id = ?) AND status = ?
|
||||
Plan:
|
||||
SEARCH rcv_files USING INDEX idx_rcv_files_status_created_at (status=?)
|
||||
|
||||
Query: UPDATE rcv_files SET status = ?, updated_at = ? WHERE rcv_file_id = ?
|
||||
Plan:
|
||||
SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?)
|
||||
@@ -1430,6 +1607,10 @@ Query: UPDATE snd_files SET status = ?, updated_at = ? WHERE snd_file_id = ?
|
||||
Plan:
|
||||
SEARCH snd_files USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query: UPDATE snd_files SET status = ?, updated_at = ? WHERE snd_file_id = ? AND status = ?
|
||||
Plan:
|
||||
SEARCH snd_files USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query: UPDATE snd_messages SET rcpt_internal_id = ?, rcpt_status = ? WHERE conn_id = ? AND internal_snd_id = ?
|
||||
Plan:
|
||||
SEARCH snd_messages USING PRIMARY KEY (conn_id=? AND internal_snd_id=?)
|
||||
|
||||
@@ -1123,6 +1123,15 @@ SEARCH m USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN
|
||||
SEARCH g USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN
|
||||
SEARCH h USING INDEX idx_sent_probe_hashes_sent_probe_id (sent_probe_id=?)
|
||||
|
||||
Query:
|
||||
SELECT s.member_relations_vector, r.index_in_group
|
||||
FROM group_members s, group_members r
|
||||
WHERE s.local_display_name = ? AND r.local_display_name = ?
|
||||
|
||||
Plan:
|
||||
SCAN s
|
||||
SEARCH r USING AUTOMATIC PARTIAL COVERING INDEX (local_display_name=?)
|
||||
|
||||
Query:
|
||||
SELECT t.roster_transfer_id, t.roster_version, t.roster_digest, t.sending_owner_gm_id, t.broker_ts,
|
||||
t.roster_msg_chat_binding, t.roster_msg_signatures, t.roster_msg_body
|
||||
@@ -1844,11 +1853,15 @@ SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query:
|
||||
UPDATE group_members
|
||||
SET contact_id = ?, updated_at = ?
|
||||
WHERE user_id = ? AND group_id = ? AND group_member_id = ?
|
||||
SET contact_id = ?, local_display_name = ?, contact_profile_id = ?, updated_at = ?
|
||||
WHERE (user_id = ? AND group_id = ? AND group_member_id = ?) OR contact_id = ?
|
||||
|
||||
Plan:
|
||||
MULTI-INDEX OR
|
||||
INDEX 1
|
||||
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
|
||||
INDEX 2
|
||||
SEARCH group_members USING INDEX idx_group_members_contact_id (contact_id=?)
|
||||
|
||||
Query:
|
||||
UPDATE group_members
|
||||
@@ -4239,15 +4252,6 @@ SEARCH c USING INTEGER PRIMARY KEY (rowid=?)
|
||||
SEARCH cs USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN
|
||||
SEARCH m USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN
|
||||
|
||||
Query:
|
||||
SELECT s.member_relations_vector, r.index_in_group
|
||||
FROM group_members s, group_members r
|
||||
WHERE s.local_display_name = ? AND r.local_display_name = ?
|
||||
|
||||
Plan:
|
||||
SCAN s
|
||||
SEARCH r USING AUTOMATIC PARTIAL COVERING INDEX (local_display_name=?)
|
||||
|
||||
Query:
|
||||
SELECT smp_server_id, host, port, key_hash, basic_auth, preset, tested, enabled,
|
||||
role_storage, role_proxy, role_names
|
||||
@@ -6714,6 +6718,10 @@ Error: SQLite3 returned ErrorError while attempting to perform prepare "explain
|
||||
Query: DELETE FROM app_settings
|
||||
Plan:
|
||||
|
||||
Query: DELETE FROM calls WHERE call_ts < ? RETURNING chat_item_id
|
||||
Plan:
|
||||
SCAN calls
|
||||
|
||||
Query: DELETE FROM calls WHERE user_id = ? AND contact_id = ?
|
||||
Plan:
|
||||
SEARCH calls USING INDEX idx_calls_contact_id (contact_id=?)
|
||||
@@ -7354,6 +7362,10 @@ Query: SELECT COUNT(1) FROM chat_items WHERE user_id = ? AND group_id = ? AND ms
|
||||
Plan:
|
||||
SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_msg_content_tag_deleted (user_id=? AND group_id=? AND msg_content_tag=? AND item_deleted=? AND item_sent=?)
|
||||
|
||||
Query: SELECT COUNT(1) FROM commands WHERE command_function = 'set_short_link'
|
||||
Plan:
|
||||
SCAN commands
|
||||
|
||||
Query: SELECT COUNT(1) FROM contacts WHERE user_id = ? AND chat_item_ttl > 0
|
||||
Plan:
|
||||
SEARCH contacts USING INDEX idx_contacts_chat_ts (user_id=?)
|
||||
@@ -7462,6 +7474,10 @@ Query: SELECT chat_item_id FROM chat_items WHERE group_scope_tag = 'member_suppo
|
||||
Plan:
|
||||
SCAN chat_items
|
||||
|
||||
Query: SELECT chat_item_id FROM chat_items WHERE item_sent = 0 AND item_text LIKE ? || '%'
|
||||
Plan:
|
||||
SCAN chat_items
|
||||
|
||||
Query: SELECT chat_item_id FROM chat_items WHERE item_text LIKE '%' || ? || '%' ORDER BY chat_item_id DESC LIMIT 1
|
||||
Plan:
|
||||
SCAN chat_items
|
||||
@@ -7538,9 +7554,9 @@ Query: SELECT connection_id FROM connections WHERE user_id = ? AND group_member_
|
||||
Plan:
|
||||
SEARCH connections USING INDEX idx_connections_group_member_id (group_member_id=?)
|
||||
|
||||
Query: SELECT contact_id FROM contacts WHERE user_id = ? AND chat_item_ttl > 0 OR chat_item_ttl IS NULL
|
||||
Query: SELECT contact_id FROM contacts WHERE user_id = ? AND (chat_item_ttl > 0 OR chat_item_ttl IS NULL)
|
||||
Plan:
|
||||
SCAN contacts
|
||||
SEARCH contacts USING INDEX idx_contacts_chat_ts (user_id=?)
|
||||
|
||||
Query: SELECT contact_id FROM contacts WHERE user_id = ? AND deleted = 0
|
||||
Plan:
|
||||
@@ -7570,6 +7586,10 @@ Query: SELECT contact_request_id FROM contact_requests WHERE user_id = ? AND loc
|
||||
Plan:
|
||||
SEARCH contact_requests USING COVERING INDEX sqlite_autoindex_contact_requests_1 (user_id=? AND local_display_name=?)
|
||||
|
||||
Query: SELECT count(1) FROM calls
|
||||
Plan:
|
||||
SCAN calls USING COVERING INDEX idx_calls_contact_id
|
||||
|
||||
Query: SELECT count(1) FROM chat_items WHERE chat_item_id > ?
|
||||
Plan:
|
||||
SEARCH chat_items USING INTEGER PRIMARY KEY (rowid>?)
|
||||
@@ -7636,9 +7656,9 @@ Query: SELECT group_id FROM groups WHERE inv_queue_info = ? AND user_id = ? LIMI
|
||||
Plan:
|
||||
SEARCH groups USING INDEX sqlite_autoindex_groups_2 (user_id=?)
|
||||
|
||||
Query: SELECT group_id FROM groups WHERE user_id = ? AND chat_item_ttl > 0 OR chat_item_ttl IS NULL
|
||||
Query: SELECT group_id FROM groups WHERE user_id = ? AND (chat_item_ttl > 0 OR chat_item_ttl IS NULL)
|
||||
Plan:
|
||||
SCAN groups
|
||||
SEARCH groups USING INDEX sqlite_autoindex_groups_2 (user_id=?)
|
||||
|
||||
Query: SELECT group_id FROM groups WHERE user_id = ? AND creating_in_progress = 1 AND created_at <= ?
|
||||
Plan:
|
||||
@@ -7676,6 +7696,10 @@ Query: SELECT image FROM contact_profiles WHERE display_name = ? LIMIT 1
|
||||
Plan:
|
||||
SEARCH contact_profiles USING INDEX contact_profiles_index (display_name=?)
|
||||
|
||||
Query: SELECT index_in_group, member_relations_vector FROM group_members WHERE local_display_name = ?
|
||||
Plan:
|
||||
SCAN group_members
|
||||
|
||||
Query: SELECT issue_failed_since, issue_error FROM badge_purchases
|
||||
Plan:
|
||||
SCAN badge_purchases
|
||||
@@ -7756,6 +7780,10 @@ Query: SELECT member_xcontact_id, member_welcome_shared_msg_id FROM group_member
|
||||
Plan:
|
||||
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query: SELECT next_wake_at, badge_purchase_id FROM badge_purchases
|
||||
Plan:
|
||||
SCAN badge_purchases
|
||||
|
||||
Query: SELECT note_folder_id FROM note_folders WHERE user_id = ?
|
||||
Plan:
|
||||
SEARCH note_folders USING COVERING INDEX note_folders_user_id (user_id=?)
|
||||
@@ -7764,6 +7792,10 @@ Query: SELECT note_folder_id FROM note_folders WHERE user_id = ? LIMIT 1
|
||||
Plan:
|
||||
SEARCH note_folders USING COVERING INDEX note_folders_user_id (user_id=?)
|
||||
|
||||
Query: SELECT purchase_key FROM badge_code_redemptions
|
||||
Plan:
|
||||
SCAN badge_code_redemptions
|
||||
|
||||
Query: SELECT quota_err_counter FROM connections WHERE user_id = ? AND connection_id = ?
|
||||
Plan:
|
||||
SEARCH connections USING INTEGER PRIMARY KEY (rowid=?)
|
||||
@@ -7824,6 +7856,10 @@ Query: SELECT shared_msg_id FROM chat_items WHERE shared_msg_id IS NOT NULL ORDE
|
||||
Plan:
|
||||
SCAN chat_items
|
||||
|
||||
Query: SELECT short_descr FROM contact_profiles WHERE display_name = ? LIMIT 1
|
||||
Plan:
|
||||
SEARCH contact_profiles USING INDEX contact_profiles_index (display_name=?)
|
||||
|
||||
Query: SELECT should_sync FROM connections_sync WHERE connections_sync_id = 1
|
||||
Plan:
|
||||
SEARCH connections_sync USING INTEGER PRIMARY KEY (rowid=?)
|
||||
@@ -7904,6 +7940,10 @@ Query: UPDATE badge_store_receipts SET provider = ?, transaction_ref = ?, paymen
|
||||
Plan:
|
||||
SEARCH badge_store_receipts USING INDEX sqlite_autoindex_badge_store_receipts_1 (invoice_id=?)
|
||||
|
||||
Query: UPDATE chat_items SET item_content = ?, item_text = ?, updated_at = ? WHERE chat_item_id = ?
|
||||
Plan:
|
||||
SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query: UPDATE chat_items SET item_msg_body = ?, item_chat_binding = ?, item_signatures = ?, item_signed_by_group_member_id = ? WHERE chat_item_id = ? AND include_in_history = 1
|
||||
Plan:
|
||||
SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
@@ -316,6 +316,13 @@ contactActive Contact {contactStatus} = contactStatus == CSActive
|
||||
contactDeleted :: Contact -> Bool
|
||||
contactDeleted Contact {contactStatus} = contactStatus == CSDeleted || contactStatus == CSDeletedByUser
|
||||
|
||||
contactUpdatableFromMember :: Contact -> Bool
|
||||
contactUpdatableFromMember ct
|
||||
| not (contactActive ct) = True
|
||||
| otherwise = case contactConn ct of
|
||||
Nothing -> True
|
||||
Just conn -> not (connReady conn) || (authErrCounter conn >= 1)
|
||||
|
||||
contactSecurityCode :: Contact -> Maybe SecurityCode
|
||||
contactSecurityCode Contact {activeConn} = connectionCode =<< activeConn
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ module Simplex.Chat.Web
|
||||
webPreviewWorker,
|
||||
writeCorsConfig,
|
||||
removeStaleFiles,
|
||||
publicGroupIdFileName,
|
||||
channelContentChanged,
|
||||
channelProfileUpdated,
|
||||
channelRemoved,
|
||||
@@ -416,9 +417,11 @@ removeStaleFiles dir activeFiles = do
|
||||
let f' = if takeExtension f == ".tmp" then dropExtension f else f
|
||||
base = dropExtension f'
|
||||
in takeExtension f' == ".json" && not (null base) && all isBase64Url base
|
||||
isBase64Url c = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_'
|
||||
isBase64Url c = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '='
|
||||
allFiles <- S.filter isPreviewFile . S.fromList <$> listDirectory dir
|
||||
mapM_ (\f -> removeFile (dir </> f)) $ S.difference allFiles activeFiles
|
||||
forM_ (S.difference allFiles activeFiles) $ \f ->
|
||||
removeFile (dir </> f) `catchOwn'` \(e :: SomeException) ->
|
||||
logError $ "web preview: error removing stale file " <> T.pack f <> ": " <> tshow e
|
||||
|
||||
toFormattedText :: Text -> Maybe MarkdownList
|
||||
toFormattedText t = case parseMaybeMarkdownList t of
|
||||
|
||||
Reference in New Issue
Block a user