mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-06 20:48:27 +00:00
Merge branch 'master' into ep/parallel-tests
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,
|
||||
@@ -123,6 +125,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)
|
||||
|
||||
|
||||
@@ -144,6 +144,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
|
||||
@@ -174,6 +175,7 @@ data ChatConfig = ChatConfig
|
||||
cleanupManagerInterval :: NominalDiffTime,
|
||||
cleanupManagerStepDelay :: Int64,
|
||||
ciExpirationInterval :: Int64, -- microseconds
|
||||
callInvitationTTL :: NominalDiffTime,
|
||||
deliveryWorkerDelay :: Int64, -- microseconds
|
||||
deliveryBucketSize :: Int,
|
||||
webPreviewConfig :: Maybe WebPreviewConfig,
|
||||
@@ -446,7 +448,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 (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)
|
||||
@@ -94,8 +97,12 @@ runSimplexChat ChatConfig {testView} ChatOpts {coreOptions = CoreChatOpts {chatR
|
||||
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
|
||||
-- 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
|
||||
|
||||
sendChatCmdStr :: ChatController -> String -> IO (Either ChatError ChatResponse)
|
||||
sendChatCmdStr cc s = runReaderT (execChatCommand CSLocal (encodeUtf8 $ T.pack s) 0) cc
|
||||
|
||||
@@ -351,7 +351,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
|
||||
@@ -1509,7 +1511,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)
|
||||
@@ -1537,9 +1540,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)
|
||||
@@ -1594,7 +1597,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
|
||||
@@ -2866,7 +2870,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
|
||||
@@ -2945,6 +2950,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])
|
||||
@@ -3980,7 +3987,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_
|
||||
_ ->
|
||||
@@ -6123,7 +6130,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 "")),
|
||||
|
||||
@@ -840,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
|
||||
@@ -2438,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
|
||||
|
||||
@@ -2446,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
|
||||
@@ -2495,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
|
||||
@@ -3045,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
|
||||
|
||||
|
||||
@@ -1250,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
|
||||
@@ -2829,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 =
|
||||
@@ -3012,15 +3013,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
|
||||
@@ -3031,9 +3040,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
|
||||
@@ -3042,18 +3048,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6647,6 +6647,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=?)
|
||||
@@ -7378,6 +7382,14 @@ 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 '!2 SB-8H8V3-PF8CV-PQMA2-A54M3!%'
|
||||
Plan:
|
||||
SCAN chat_items
|
||||
|
||||
Query: SELECT chat_item_id FROM chat_items WHERE item_sent = 0 AND item_text LIKE '!2 SB-Y14GX-Z83KW-0E3PK-DEN7X!%'
|
||||
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
|
||||
@@ -7486,6 +7498,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>?)
|
||||
@@ -7676,6 +7692,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=?)
|
||||
@@ -7796,6 +7816,10 @@ Query: UPDATE badge_purchases SET next_wake_at = ? WHERE badge_purchase_id = ?
|
||||
Plan:
|
||||
SEARCH badge_purchases USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
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=?)
|
||||
|
||||
@@ -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