mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-25 11:14:28 +00:00
core: attach badge proofs to files over size limit (#7455)
* core: attach badge proofs to files over size limit * types * more types * implement file badge proofs * tests * move file limits to config, add tests * more tests, work correctly in "send as group" case * fix races in tests * group badge tests * query plans * add history support, fixes * simplify * refactor * refactor * type * restructure schema for proofs * rename, refactor * refactor * fix, refactor * refactor * ui * comments * alerts * update nix, ios library * updare sharing * update simplexmq * update text * improve messages * api types * postgres schema * fix * test * query plans --------- Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
This commit is contained in:
co-authored by
Evgeny @ SimpleX Chat
parent
47d32b674a
commit
5ffbe733a8
+2
-1
@@ -28,7 +28,7 @@ import qualified Data.Map.Strict as M
|
||||
import Data.Maybe (fromMaybe, mapMaybe)
|
||||
import Data.Text (Text)
|
||||
import Data.Time.Clock (getCurrentTime, nominalDay)
|
||||
import Simplex.Chat.Badges (badgeServerCredential)
|
||||
import Simplex.Chat.Badges (badgeServerCredential, defaultFileSizeLimits)
|
||||
import Simplex.Chat.Controller
|
||||
import Simplex.Chat.Library.Commands
|
||||
import Simplex.Chat.Operators
|
||||
@@ -106,6 +106,7 @@ defaultChatConfig =
|
||||
xftpDescrPartSize = 14000,
|
||||
inlineFiles = defaultInlineFilesConfig,
|
||||
autoAcceptFileSize = 0,
|
||||
fileSizeLimits = defaultFileSizeLimits,
|
||||
showReactions = False,
|
||||
showFullLinks = False,
|
||||
showReceipts = False,
|
||||
|
||||
+114
-9
@@ -25,7 +25,11 @@ module Simplex.Chat.Badges
|
||||
BBSPublicKeyStr (..),
|
||||
localBadgeInfo,
|
||||
localBadgeStatus,
|
||||
FileSizeLimits (..),
|
||||
defaultFileSizeLimits,
|
||||
maxXFTPFileSize,
|
||||
maxSndXFTPFileSize,
|
||||
badgeSndGraceInterval,
|
||||
badgeServerCredential,
|
||||
maxFileSizeSupporter,
|
||||
maxFileSizeLegend,
|
||||
@@ -46,6 +50,10 @@ module Simplex.Chat.Badges
|
||||
verifyBadge_,
|
||||
mkBadgeStatus,
|
||||
BadgeRow,
|
||||
BadgeProofKind (..),
|
||||
BadgeProofRow,
|
||||
badgeProofToRow,
|
||||
rowToBadgeProof,
|
||||
badgeToRow,
|
||||
localBadgeToRow,
|
||||
rowToBadge,
|
||||
@@ -66,11 +74,13 @@ import Data.String
|
||||
import Data.Text (Text)
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Data.Time.Clock (NominalDiffTime, UTCTime, addUTCTime, nominalDay)
|
||||
import Data.Time.Clock.System (systemToUTCTime, utcToSystemTime)
|
||||
import Simplex.FileTransfer.Description (gb, maxFileSize)
|
||||
import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..), fromTextField_)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.BBS
|
||||
import Simplex.Messaging.Crypto.Entitlement (Entitlement (Entitlement), EntitlementCredential (EntitlementCredential), MasterKey (MasterKey), entitlementBBSHeader)
|
||||
import Simplex.Messaging.Encoding (Encoding (..))
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON)
|
||||
#if defined(dbPostgres)
|
||||
@@ -114,6 +124,35 @@ instance FromJSON BadgeType where
|
||||
data BadgeStatus = BSActive | BSExpired | BSExpiredOld | BSFailed | BSUnknownKey
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance TextEncoding BadgeStatus where
|
||||
textEncode = \case
|
||||
BSActive -> "active"
|
||||
BSExpired -> "expired"
|
||||
BSExpiredOld -> "expired_old"
|
||||
BSFailed -> "failed"
|
||||
BSUnknownKey -> "unknown_key"
|
||||
textDecode = \case
|
||||
"active" -> Just BSActive
|
||||
"expired" -> Just BSExpired
|
||||
"expired_old" -> Just BSExpiredOld
|
||||
"failed" -> Just BSFailed
|
||||
"unknown_key" -> Just BSUnknownKey
|
||||
_ -> Nothing
|
||||
|
||||
-- Badge proof kind - a file has at most one proof of each kind
|
||||
|
||||
data BadgeProofKind = BPKInvitation | BPKDescription
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance TextEncoding BadgeProofKind where
|
||||
textEncode = \case
|
||||
BPKInvitation -> "inv"
|
||||
BPKDescription -> "descr"
|
||||
textDecode = \case
|
||||
"inv" -> Just BPKInvitation
|
||||
"descr" -> Just BPKDescription
|
||||
_ -> Nothing
|
||||
|
||||
-- Disclosed badge content (BBS messages 1, 2, 3)
|
||||
|
||||
data BadgeInfo = BadgeInfo
|
||||
@@ -186,10 +225,10 @@ localBadgeStatus = \case
|
||||
ShownBadge _ st -> st
|
||||
|
||||
-- XFTP file size limit raised by an active badge: a legend badge to 5GB, any other to 2GB, otherwise the default.
|
||||
maxFileSizeSupporter :: Int64
|
||||
maxFileSizeSupporter :: Integer
|
||||
maxFileSizeSupporter = gb 2
|
||||
|
||||
maxFileSizeLegend :: Int64
|
||||
maxFileSizeLegend :: Integer
|
||||
maxFileSizeLegend = gb 5
|
||||
|
||||
badgeServerCredential :: Maybe LocalBadge -> Maybe EntitlementCredential
|
||||
@@ -198,31 +237,62 @@ badgeServerCredential = \case
|
||||
Just $ EntitlementCredential (fromIntegral idx) (MasterKey mk) (Entitlement badgeExpiry (textEncode badgeType) badgeExtra) sig
|
||||
_ -> Nothing
|
||||
|
||||
maxXFTPFileSize :: Maybe LocalBadge -> Int64
|
||||
maxXFTPFileSize = \case
|
||||
Just b | localBadgeStatus b == BSActive -> case badgeType (localBadgeInfo b) of
|
||||
BTLegend -> maxFileSizeLegend
|
||||
_ -> maxFileSizeSupporter
|
||||
_ -> maxFileSize
|
||||
data FileSizeLimits = FileSizeLimits
|
||||
{ noBadge :: Integer,
|
||||
supporter :: Integer,
|
||||
legend :: Integer
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
defaultFileSizeLimits :: FileSizeLimits
|
||||
defaultFileSizeLimits = FileSizeLimits {noBadge = toInteger maxFileSize, supporter = maxFileSizeSupporter, legend = maxFileSizeLegend}
|
||||
|
||||
-- a badge raises the size limit at send for this long after its expiry, shorter than badgeGraceInterval so the receiver still accepts the size
|
||||
badgeSndGraceInterval :: NominalDiffTime
|
||||
badgeSndGraceInterval = nominalDay
|
||||
|
||||
badgeFileSize :: FileSizeLimits -> LocalBadge -> Integer
|
||||
badgeFileSize FileSizeLimits {supporter, legend} b = case badgeType (localBadgeInfo b) of
|
||||
BTLegend -> legend
|
||||
_ -> supporter
|
||||
|
||||
maxXFTPFileSize :: FileSizeLimits -> Maybe LocalBadge -> Integer
|
||||
maxXFTPFileSize lims = \case
|
||||
Just b | localBadgeStatus b == BSActive -> badgeFileSize lims b
|
||||
_ -> noBadge lims
|
||||
|
||||
maxSndXFTPFileSize :: FileSizeLimits -> UTCTime -> Maybe LocalBadge -> Integer
|
||||
maxSndXFTPFileSize lims now = \case
|
||||
Just b | localBadgeStatus b == BSActive && addUTCTime badgeSndGraceInterval (badgeExpiry (localBadgeInfo b)) >= now -> badgeFileSize lims b
|
||||
_ -> noBadge lims
|
||||
|
||||
-- Presentation header: a tag char + payload. PHTest is unbound - a fresh random nonce per
|
||||
-- presentation, not bound to any context; the 'T' tag marks it so master rejects it.
|
||||
-- PHUnknown is the forward-compat catch-all for tags this version does not interpret.
|
||||
|
||||
data ProofPresHeaderTag = PHTestTag | PHUnknownTag Char
|
||||
data ProofPresHeaderTag = PHTestTag | PHChatTag | PHFileInvTag | PHFileDescrTag | PHUnknownTag Char
|
||||
|
||||
instance StrEncoding ProofPresHeaderTag where
|
||||
strEncode = B.singleton . \case
|
||||
PHTestTag -> 'T'
|
||||
PHChatTag -> 'C'
|
||||
PHFileInvTag -> 'F'
|
||||
PHFileDescrTag -> 'D'
|
||||
PHUnknownTag c -> c
|
||||
strP = tag <$> A.anyChar
|
||||
where
|
||||
tag = \case
|
||||
'T' -> PHTestTag
|
||||
'C' -> PHChatTag
|
||||
'F' -> PHFileInvTag
|
||||
'D' -> PHFileDescrTag
|
||||
c -> PHUnknownTag c
|
||||
|
||||
data ProofPresHeader
|
||||
= PHTest ByteString
|
||||
| PHChat ByteString
|
||||
| PHFileInv {chatBinding :: ByteString, fileSize :: Int64}
|
||||
| PHFileDescr {chatBinding :: ByteString, fileSize :: Int64, descrHash :: ByteString, fileExpires :: Maybe UTCTime}
|
||||
| PHUnknown Char ByteString
|
||||
deriving (Eq, Show)
|
||||
deriving (ToJSON, FromJSON) via (StrJSON "ProofPresHeader" ProofPresHeader)
|
||||
@@ -230,16 +300,31 @@ data ProofPresHeader
|
||||
instance StrEncoding ProofPresHeader where
|
||||
strEncode = \case
|
||||
PHTest nonce -> strEncode PHTestTag <> nonce
|
||||
PHChat binding -> strEncode PHChatTag <> binding
|
||||
PHFileInv {chatBinding, fileSize} ->
|
||||
strEncode PHFileInvTag <> smpEncode (chatBinding, fileSize)
|
||||
PHFileDescr {chatBinding, fileSize, descrHash, fileExpires} ->
|
||||
strEncode PHFileDescrTag <> smpEncode (chatBinding, fileSize, descrHash, utcToSystemTime <$> fileExpires)
|
||||
PHUnknown c b -> strEncode (PHUnknownTag c) <> b
|
||||
strP =
|
||||
strP >>= \case
|
||||
PHTestTag -> PHTest <$> A.takeByteString
|
||||
PHChatTag -> PHChat <$> A.takeByteString
|
||||
PHFileInvTag -> do
|
||||
(chatBinding, fileSize) <- smpP
|
||||
pure PHFileInv {chatBinding, fileSize}
|
||||
PHFileDescrTag -> do
|
||||
(chatBinding, fileSize, descrHash, expires_) <- smpP
|
||||
pure PHFileDescr {chatBinding, fileSize, descrHash, fileExpires = systemToUTCTime <$> expires_}
|
||||
PHUnknownTag c -> PHUnknown c <$> A.takeByteString
|
||||
|
||||
-- v6.5.x accepts both; v7 will reject PHTest/PHUnknown
|
||||
proofPresHeaderAccepted :: ProofPresHeader -> Bool
|
||||
proofPresHeaderAccepted = \case
|
||||
PHTest _ -> True
|
||||
PHChat _ -> True
|
||||
PHFileInv {} -> True
|
||||
PHFileDescr {} -> True
|
||||
PHUnknown _ _ -> True
|
||||
|
||||
-- Payment proof
|
||||
@@ -348,6 +433,26 @@ instance FromField BadgeType where fromField = fromTextField_ textDecode
|
||||
|
||||
instance ToField BadgeType where toField = toField . textEncode
|
||||
|
||||
instance FromField BadgeStatus where fromField = fromTextField_ textDecode
|
||||
|
||||
instance ToField BadgeStatus where toField = toField . textEncode
|
||||
|
||||
instance FromField BadgeProofKind where fromField = fromTextField_ textDecode
|
||||
|
||||
instance ToField BadgeProofKind where toField = toField . textEncode
|
||||
|
||||
-- (proof, pres_header, key_idx, type, expiry, extra) - the fields of BadgeProof as stored in file_badge_proofs
|
||||
type BadgeProofRow = (Binary ByteString, Binary ByteString, Int, Text, UTCTime, Text)
|
||||
|
||||
badgeProofToRow :: BadgeProof -> BadgeProofRow
|
||||
badgeProofToRow (BadgeProof idx (BBSPresHeader ph) (BBSProof p) BadgeInfo {badgeType, badgeExpiry, badgeExtra}) =
|
||||
(Binary p, Binary ph, idx, textEncode badgeType, badgeExpiry, badgeExtra)
|
||||
|
||||
rowToBadgeProof :: BadgeProofRow -> Maybe BadgeProof
|
||||
rowToBadgeProof (Binary p, Binary ph, idx, type_, badgeExpiry, badgeExtra) = do
|
||||
badgeType <- textDecode type_
|
||||
pure $ BadgeProof idx (BBSPresHeader ph) (BBSProof p) BadgeInfo {badgeType, badgeExpiry, badgeExtra}
|
||||
|
||||
-- (proof, pres_header, expiry, type, verified, extra, master_key, signature, key_idx) - binary columns wrapped in Binary (BLOB/bytea)
|
||||
type BadgeRow = (Maybe (Binary ByteString), Maybe (Binary ByteString), Maybe UTCTime, Maybe Text, Maybe BoolInt, Maybe Text, Maybe (Binary ByteString), Maybe (Binary ByteString), Maybe Int)
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ import Simplex.Messaging.Agent.Store.DB (SQLError)
|
||||
import qualified Simplex.Messaging.Agent.Store.DB as DB
|
||||
import Simplex.Messaging.Client (HostMode (..), SMPProxyFallback (..), SMPProxyMode (..), SMPWebPortServers (..), SocksMode (..))
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Chat.Badges (BadgeCredential)
|
||||
import Simplex.Chat.Badges (BadgeCredential, FileSizeLimits)
|
||||
import Simplex.Messaging.Crypto.BBS (BBSPublicKey)
|
||||
import Simplex.Messaging.Crypto.File (CryptoFile (..))
|
||||
import qualified Simplex.Messaging.Crypto.File as CF
|
||||
@@ -152,6 +152,7 @@ data ChatConfig = ChatConfig
|
||||
xftpDescrPartSize :: Int,
|
||||
inlineFiles :: InlineFilesConfig,
|
||||
autoAcceptFileSize :: Integer,
|
||||
fileSizeLimits :: FileSizeLimits,
|
||||
showReactions :: Bool,
|
||||
showFullLinks :: Bool,
|
||||
showReceipts :: Bool,
|
||||
|
||||
@@ -56,7 +56,7 @@ import Data.Type.Equality
|
||||
import qualified Data.UUID as UUID
|
||||
import qualified Data.UUID.V4 as V4
|
||||
import Simplex.Chat.Library.Subscriber
|
||||
import Simplex.Chat.Badges (BadgeCredential (..), LocalBadge (..), badgeServerCredential, maxXFTPFileSize, mkBadgeStatus, verifyCredential)
|
||||
import Simplex.Chat.Badges (BadgeCredential (..), LocalBadge (..), badgeServerCredential, maxSndXFTPFileSize, mkBadgeStatus, verifyCredential)
|
||||
import Simplex.Chat.Names (SimplexDomainProof (..), SimplexDomainClaim (..), claimDomain, mkDomainClaim)
|
||||
import Simplex.Chat.Call
|
||||
import Simplex.Chat.Controller
|
||||
@@ -3626,7 +3626,7 @@ processChatCommand cxt nm = \case
|
||||
fsFilePath <- lift $ toFSFilePath filePath
|
||||
fileSize <- liftIO $ CF.getFileContentsSize file {filePath = fsFilePath}
|
||||
when (fileSize > toInteger maxFileSizeHard) $ throwChatError $ CEFileSize filePath
|
||||
(_, _, fileTransferMeta) <- xftpSndFileTransfer_ user file fileSize 1 Nothing
|
||||
(_, _, fileTransferMeta) <- xftpSndFileTransfer_ user file fileSize 1 Nothing Nothing
|
||||
pure CRSndStandaloneFileCreated {user, fileTransferMeta}
|
||||
APIStandaloneFileInfo FileDescriptionURI {clientData} -> pure . CRStandaloneFileInfo $ clientData >>= J.decodeStrict . encodeUtf8
|
||||
APIDownloadStandaloneFile userId uri file -> withUserId userId $ \user -> do
|
||||
@@ -3975,7 +3975,9 @@ processChatCommand cxt nm = \case
|
||||
fsFilePath <- lift $ toFSFilePath f
|
||||
unlessM (doesFileExist fsFilePath) . throwChatError $ CEFileNotFound f
|
||||
fileSize <- liftIO $ CF.getFileContentsSize $ CryptoFile fsFilePath cfArgs
|
||||
when (fromInteger fileSize > maxXFTPFileSize sndBadge) $ throwChatError $ CEFileSize f
|
||||
lims <- asks $ fileSizeLimits . config
|
||||
now <- liftIO getCurrentTime
|
||||
when (fileSize > maxSndXFTPFileSize lims now sndBadge) $ throwChatError $ CEFileSize f
|
||||
pure fileSize
|
||||
updateProfile :: User -> Profile -> CM ChatResponse
|
||||
updateProfile user p' = updateProfile_ user p' True $ withFastStore $ \db -> updateUserProfile db user p'
|
||||
@@ -4771,7 +4773,8 @@ processChatCommand cxt nm = \case
|
||||
Just file -> do
|
||||
let User {profile = LocalProfile {localBadge}} = user
|
||||
fileSize <- checkSndFile (if contactConnIncognito ct then Nothing else localBadge) file
|
||||
(fInv, ciFile) <- xftpSndFileTransfer user file fileSize 1 $ CGContact ct
|
||||
binding_ <- ifM ((not (contactConnIncognito ct) &&) <$> fileNeedsBadge fileSize) (directChatBinding ct) (pure Nothing)
|
||||
(fInv, ciFile) <- xftpSndFileTransfer user file fileSize 1 (CGContact ct) binding_
|
||||
pure (Just fInv, Just ciFile)
|
||||
Nothing -> pure (Nothing, Nothing)
|
||||
prepareMsgs :: NonEmpty (ComposedMessageReq, Maybe FileInvitation) -> Maybe CITimed -> CM (NonEmpty (MsgContainer, Maybe (CIQuote 'CTDirect)))
|
||||
@@ -4802,8 +4805,10 @@ processChatCommand cxt nm = \case
|
||||
sendGroupContentMessages user gInfo scope showGroupAsSender live itemTTL sign cmrs = do
|
||||
assertMultiSendable live cmrs
|
||||
chatScopeInfo <- mapM (getChatScopeInfo cxt user) scope
|
||||
recipients <- getGroupRecipients cxt user gInfo chatScopeInfo modsCompatVersion
|
||||
sendGroupContentMessages_ user gInfo scope showGroupAsSender chatScopeInfo recipients live itemTTL sign cmrs
|
||||
-- the member key is created before the send, so that signatures and file badge proofs assert the same key
|
||||
gInfo' <- createUserMemberKey gInfo
|
||||
recipients <- getGroupRecipients cxt user gInfo' chatScopeInfo modsCompatVersion
|
||||
sendGroupContentMessages_ user gInfo' scope showGroupAsSender chatScopeInfo recipients live itemTTL sign cmrs
|
||||
where
|
||||
hasReport = any (\(ComposedMessage {msgContent}, _, _, _) -> isReport msgContent) cmrs
|
||||
modsCompatVersion = if hasReport then contentReportsVersion else groupKnockingVersion
|
||||
@@ -4856,7 +4861,9 @@ processChatCommand cxt nm = \case
|
||||
Just file -> do
|
||||
let User {profile = LocalProfile {localBadge}} = user
|
||||
fileSize <- checkSndFile (if incognitoMembership gInfo then Nothing else localBadge) file
|
||||
(fInv, ciFile) <- xftpSndFileTransfer user file fileSize n $ CGGroup gInfo recipients
|
||||
needsBadge <- fileNeedsBadge fileSize
|
||||
let binding_ = if needsBadge && not (incognitoMembership gInfo) then sndGroupChatBinding gInfo showGroupAsSender else Nothing
|
||||
(fInv, ciFile) <- xftpSndFileTransfer user file fileSize n (CGGroup gInfo recipients) binding_
|
||||
fInv' <-
|
||||
if signMsgs && useRelays' gInfo
|
||||
then (\d -> (fInv :: FileInvitation) {fileDigest = Just d}) <$> cryptoFileDigest file
|
||||
@@ -4914,9 +4921,9 @@ processChatCommand cxt nm = \case
|
||||
-- batching retrieval of quoted messages (prepareMsgs).
|
||||
when (live || length (L.filter (\(ComposedMessage {quotedItemId}, _, _, _) -> isJust quotedItemId) cmrs) > 1) $
|
||||
throwCmdError "invalid multi send: live and more than one quote not supported"
|
||||
xftpSndFileTransfer :: User -> CryptoFile -> Integer -> Int -> ContactOrGroup -> CM (FileInvitation, CIFile 'MDSnd)
|
||||
xftpSndFileTransfer user file fileSize n contactOrGroup = do
|
||||
(fInv, ciFile, ft) <- xftpSndFileTransfer_ user file fileSize n $ Just contactOrGroup
|
||||
xftpSndFileTransfer :: User -> CryptoFile -> Integer -> Int -> ContactOrGroup -> Maybe ByteString -> CM (FileInvitation, CIFile 'MDSnd)
|
||||
xftpSndFileTransfer user file fileSize n contactOrGroup binding_ = do
|
||||
(fInv, ciFile, ft) <- xftpSndFileTransfer_ user file fileSize n (Just contactOrGroup) binding_
|
||||
case contactOrGroup of
|
||||
CGContact Contact {activeConn} -> forM_ activeConn $ \conn ->
|
||||
withFastStore' $ \db -> createSndFTDescrXFTP db user Nothing conn ft dummyFileDescr
|
||||
@@ -5006,7 +5013,7 @@ processChatCommand cxt nm = \case
|
||||
chunkSize <- asks $ fileChunkSize . config
|
||||
withFastStore' $ \db -> do
|
||||
fileId <- createLocalFile CIFSSndStored db user nf createdAt cf fileSize chunkSize
|
||||
pure CIFile {fileId, fileName = takeFileName filePath, fileSize, fileSource = Just cf, fileStatus = CIFSSndStored, fileProtocol = FPLocal, fileExpires = Nothing}
|
||||
pure CIFile {fileId, fileName = takeFileName filePath, fileSize, fileSource = Just cf, fileStatus = CIFSSndStored, fileProtocol = FPLocal, fileExpires = Nothing, fileProhibited = Nothing}
|
||||
prepareLocalItemsData ::
|
||||
NonEmpty ComposedMessageReq ->
|
||||
NonEmpty (Maybe (CIFile 'MDSnd)) ->
|
||||
|
||||
@@ -53,7 +53,7 @@ import Data.Text.Encoding (encodeUtf8)
|
||||
import Data.Time (addUTCTime)
|
||||
import Data.Time.Calendar (fromGregorian)
|
||||
import Data.Time.Clock (UTCTime (..), diffUTCTime, getCurrentTime, nominalDiffTimeToSeconds, secondsToDiffTime)
|
||||
import Simplex.Chat.Badges (BadgeCredential (..), ProofPresHeader (..), BadgeProof (..), BadgeStatus (..), LocalBadge (..), badgeProof, mkBadgeStatus, verifyBadge)
|
||||
import Simplex.Chat.Badges (BadgeCredential (..), BadgeInfo (..), ProofPresHeader (..), BadgeProof (..), BadgeProofKind (..), BadgeStatus (..), FileSizeLimits (..), LocalBadge (..), badgeProof, badgeSndGraceInterval, generateBadgeProof, localBadgeStatus, maxXFTPFileSize, mkBadgeStatus, verifyBadge)
|
||||
import Simplex.Chat.Names (SimplexDomainClaim (..), claimDomain)
|
||||
import Simplex.Chat.Call
|
||||
import Simplex.Chat.Controller
|
||||
@@ -98,7 +98,8 @@ import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
|
||||
import qualified Simplex.Messaging.Crypto.File as CF
|
||||
import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn)
|
||||
import qualified Simplex.Messaging.Crypto.Ratchet as CR
|
||||
import Simplex.Messaging.Encoding (smpEncode)
|
||||
import Simplex.Messaging.Crypto.BBS (BBSPresHeader (..))
|
||||
import Simplex.Messaging.Encoding (smpDecode, smpEncode)
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (MsgBody, MsgFlags (..), ProtoServerWithAuth (..), ProtocolServer, ProtocolTypeI (..), SProtocolType (..), SubscriptionMode (..), UserProtocol, XFTPServer)
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
@@ -435,10 +436,11 @@ roundedFDCount n
|
||||
| n <= 0 = 4
|
||||
| otherwise = max 4 $ fromIntegral $ (2 :: Integer) ^ (ceiling (logBase 2 (fromIntegral n) :: Double) :: Integer)
|
||||
|
||||
xftpSndFileTransfer_ :: User -> CryptoFile -> Integer -> Int -> Maybe ContactOrGroup -> CM (FileInvitation, CIFile 'MDSnd, FileTransferMeta)
|
||||
xftpSndFileTransfer_ user file@(CryptoFile filePath cfArgs) fileSize n contactOrGroup_ = do
|
||||
xftpSndFileTransfer_ :: User -> CryptoFile -> Integer -> Int -> Maybe ContactOrGroup -> Maybe ByteString -> CM (FileInvitation, CIFile 'MDSnd, FileTransferMeta)
|
||||
xftpSndFileTransfer_ user file@(CryptoFile filePath cfArgs) fileSize n contactOrGroup_ binding_ = do
|
||||
fileBadge <- pure binding_ $>>= \chatBinding -> sndBadgeProof user PHFileInv {chatBinding, fileSize = fromInteger fileSize}
|
||||
let fileName = takeFileName filePath
|
||||
fInv = xftpFileInvitation fileName fileSize dummyFileDescr
|
||||
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
|
||||
@@ -446,9 +448,32 @@ xftpSndFileTransfer_ user file@(CryptoFile filePath cfArgs) fileSize n contactOr
|
||||
chSize <- asks $ fileChunkSize . config
|
||||
ft@FileTransferMeta {fileId} <- withStore' $ \db -> createSndFileTransferXFTP db user contactOrGroup_ file fInv (AgentSndFileId aFileId) Nothing chSize
|
||||
let fileSource = Just $ CryptoFile filePath cfArgs
|
||||
ciFile = CIFile {fileId, fileName, fileSize, fileSource, fileStatus = CIFSSndStored, fileProtocol = FPXFTP, fileExpires = Nothing}
|
||||
ciFile = CIFile {fileId, fileName, fileSize, fileSource, fileStatus = CIFSSndStored, fileProtocol = FPXFTP, fileExpires = Nothing, fileProhibited = Nothing}
|
||||
pure (fInv, ciFile, ft)
|
||||
|
||||
fileNeedsBadge :: Integer -> CM Bool
|
||||
fileNeedsBadge fileSize = (fileSize >) . noBadge <$> asks (fileSizeLimits . config)
|
||||
|
||||
sndBadgeProof :: User -> ProofPresHeader -> CM (Maybe BadgeProof)
|
||||
sndBadgeProof user = sndBadgeProof_ user . BBSPresHeader . strEncode
|
||||
|
||||
sndBadgeProof_ :: User -> BBSPresHeader -> CM (Maybe BadgeProof)
|
||||
sndBadgeProof_ User {profile = LocalProfile {localBadge}} ph = case localBadge of
|
||||
Just (OwnBadge cred@(BadgeCredential keyIdx _ _ _) _) -> do
|
||||
keys <- asks $ badgePublicKeys . config
|
||||
case M.lookup keyIdx keys of
|
||||
Nothing -> Nothing <$ logError "sndBadgeProof: badge key index not in config"
|
||||
Just key ->
|
||||
liftIO (generateBadgeProof key cred ph) >>= \case
|
||||
Right proof -> pure $ Just proof
|
||||
Left e -> Nothing <$ logError ("sndBadgeProof: proof generation failed: " <> T.pack e)
|
||||
_ -> pure Nothing
|
||||
|
||||
sndGroupChatBinding :: GroupInfo -> ShowGroupAsSender -> Maybe ByteString
|
||||
sndGroupChatBinding GroupInfo {groupKeys, membership = GroupMember {memberId}} asGroup
|
||||
| asGroup = (\PublicGroupKeys {publicGroupId} -> encodeChatBinding CBChannel $ smpEncode publicGroupId) <$> (groupKeys >>= publicGroupKeys)
|
||||
| otherwise = (\GroupKeys {memberPrivKey} -> encodeChatBinding CBGroup $ groupBindingData groupKeys memberId (C.publicKey memberPrivKey)) <$> groupKeys
|
||||
|
||||
cryptoFileDigest :: CryptoFile -> CM FD.FileDigest
|
||||
cryptoFileDigest (CryptoFile filePath cfArgs) = do
|
||||
fsPath <- lift $ toFSFilePath filePath
|
||||
@@ -744,10 +769,11 @@ rctFileCancelled = \case
|
||||
_ -> False
|
||||
|
||||
acceptFileReceive :: User -> RcvFileTransfer -> Bool -> Maybe Bool -> Maybe FilePath -> CM AChatItem
|
||||
acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = FileInvitation {fileName = fName, fileConnReq, fileInline, fileSize}, fileStatus, grpMemberId, cryptoArgs} userApprovedRelays rcvInline_ filePath_ = do
|
||||
acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = FileInvitation {fileName = fName, fileConnReq, fileInline, fileSize}, fileProhibited, fileStatus, grpMemberId, cryptoArgs} userApprovedRelays rcvInline_ filePath_ = do
|
||||
unless (fileStatus == RFSNew) $ case fileStatus of
|
||||
RFSCancelled _ -> throwChatError $ CEFileCancelled fName
|
||||
_ -> throwChatError $ CEFileAlreadyReceiving fName
|
||||
when (isJust fileProhibited) $ throwChatError $ CEFileSize fName
|
||||
cxt <- chatStoreCxt
|
||||
case (xftpRcvFile, fileConnReq) of
|
||||
-- XFTP
|
||||
@@ -1419,15 +1445,17 @@ sendHistory user gInfo@GroupInfo {membership} m@GroupMember {activeConn = Just c
|
||||
resolveAuthor (Just gmId) = do
|
||||
cxt <- chatStoreCxt
|
||||
eitherToMaybe <$> withStore' (\db -> runExceptT $ getGroupMemberById db cxt user gmId)
|
||||
getRcvFileInvDescr :: CIFile 'MDRcv -> CM (Maybe (FileInvitation, RcvFileDescrText, Maybe UTCTime))
|
||||
getRcvFileInvDescr :: CIFile 'MDRcv -> CM (Maybe HistoryFile)
|
||||
getRcvFileInvDescr ciFile@CIFile {fileId, fileProtocol, fileStatus, fileExpires} = do
|
||||
expired <- fileExpired fileExpires
|
||||
if fileProtocol /= FPXFTP || fileStatus == CIFSRcvCancelled || expired
|
||||
then pure Nothing
|
||||
else do
|
||||
rfd <- withStore $ \db -> getRcvFileDescrByRcvFileId db fileId
|
||||
pure $ invCompleteDescr ciFile rfd
|
||||
getSndFileInvDescr :: CIFile 'MDSnd -> CM (Maybe (FileInvitation, RcvFileDescrText, Maybe UTCTime))
|
||||
(rfd, (invBadge, descrBadge)) <- withStore $ \db -> do
|
||||
rfd <- getRcvFileDescrByRcvFileId db fileId
|
||||
(rfd,) <$> liftIO (getFileBadgeProofs db fileId)
|
||||
pure $ invCompleteDescr ciFile rfd invBadge descrBadge
|
||||
getSndFileInvDescr :: CIFile 'MDSnd -> CM (Maybe HistoryFile)
|
||||
getSndFileInvDescr ciFile@CIFile {fileId, fileProtocol, fileStatus, fileExpires} = do
|
||||
expired <- fileExpired fileExpires
|
||||
if fileProtocol /= FPXFTP || fileStatus == CIFSSndCancelled || expired
|
||||
@@ -1435,28 +1463,54 @@ sendHistory user gInfo@GroupInfo {membership} m@GroupMember {activeConn = Just c
|
||||
else do
|
||||
-- can also lookup in extra_xftp_file_descriptions, though it can be empty;
|
||||
-- would be best if snd file had a single rcv description for all members saved in files table
|
||||
rfd <- withStore $ \db -> getRcvFileDescrBySndFileId db fileId
|
||||
pure $ invCompleteDescr ciFile rfd
|
||||
now <- liftIO getCurrentTime
|
||||
(rfd, (invBadge, descrBadge)) <- withStore $ \db -> do
|
||||
rfd <- getRcvFileDescrBySndFileId db fileId
|
||||
(rfd,) <$> liftIO (getFileBadgeProofs db fileId)
|
||||
-- a signed item forwards the author's original bytes, so its invitation proof cannot be replaced
|
||||
(invBadge', descrBadge') <-
|
||||
if isNothing signedMsg_ && ownBadgeActive && (staleBadge now invBadge || staleBadge now descrBadge)
|
||||
then refreshSndBadges fileId invBadge descrBadge
|
||||
else pure (invBadge, descrBadge)
|
||||
pure $ invCompleteDescr ciFile rfd invBadge' descrBadge'
|
||||
staleBadge :: UTCTime -> Maybe BadgeProof -> Bool
|
||||
staleBadge now = \case
|
||||
Just BadgeProof {badgeInfo = BadgeInfo {badgeExpiry}} -> addUTCTime badgeSndGraceInterval badgeExpiry < now
|
||||
Nothing -> False
|
||||
ownBadgeActive :: Bool
|
||||
ownBadgeActive = maybe False ((BSActive ==) . localBadgeStatus) localBadge
|
||||
where
|
||||
User {profile = LocalProfile {localBadge}} = user
|
||||
-- both proofs are made with the same badge, and are re-made with the current badge over the stored headers
|
||||
refreshSndBadges :: FileTransferId -> Maybe BadgeProof -> Maybe BadgeProof -> CM (Maybe BadgeProof, Maybe BadgeProof)
|
||||
refreshSndBadges fileId invBadge descrBadge = do
|
||||
invBadge' <- mapM reProve invBadge
|
||||
descrBadge' <- mapM reProve descrBadge
|
||||
withStore' $ \db -> do
|
||||
forM_ invBadge' $ createFileBadgeProof db fileId BPKInvitation
|
||||
forM_ descrBadge' $ createFileBadgeProof db fileId BPKDescription
|
||||
pure (invBadge', descrBadge')
|
||||
where
|
||||
reProve badge@BadgeProof {presHeader} = fromMaybe badge <$> sndBadgeProof_ user presHeader
|
||||
fileExpired :: Maybe UTCTime -> CM Bool
|
||||
fileExpired fileExpires = do
|
||||
ttl <- asks $ rcvFilesTTL . agentConfig . config
|
||||
now <- liftIO getCurrentTime
|
||||
pure $ fromMaybe (addUTCTime ttl $ chatItemTs cci) fileExpires < now
|
||||
invCompleteDescr :: CIFile d -> RcvFileDescr -> Maybe (FileInvitation, RcvFileDescrText, Maybe UTCTime)
|
||||
invCompleteDescr CIFile {fileName, fileSize, fileExpires} RcvFileDescr {fileDescrText, fileDescrComplete}
|
||||
invCompleteDescr :: CIFile d -> RcvFileDescr -> Maybe BadgeProof -> Maybe BadgeProof -> Maybe HistoryFile
|
||||
invCompleteDescr CIFile {fileName, fileSize, fileExpires} RcvFileDescr {fileDescrText, fileDescrComplete} invBadge descrBadge
|
||||
| fileDescrComplete =
|
||||
let fInvDescr = FileDescr {fileDescrText = "", fileDescrPartNo = 0, fileDescrComplete = False}
|
||||
fInv = xftpFileInvitation fileName fileSize fInvDescr
|
||||
in Just (fInv, fileDescrText, fileExpires)
|
||||
let fInv = (xftpFileInvitation fileName fileSize dummyFileDescr :: FileInvitation) {fileBadge = invBadge}
|
||||
in Just (fInv, fileDescrText, fileExpires, descrBadge)
|
||||
| otherwise = Nothing
|
||||
processContentItem :: Maybe GroupMember -> ChatItem 'CTGroup d -> MsgContent -> Maybe (FileInvitation, RcvFileDescrText, Maybe UTCTime) -> CM [(GrpMsgForward, VerifiedMsg 'Json)]
|
||||
processContentItem :: Maybe GroupMember -> ChatItem 'CTGroup d -> MsgContent -> Maybe HistoryFile -> CM [(GrpMsgForward, VerifiedMsg 'Json)]
|
||||
processContentItem member_ ChatItem {formattedText, meta, quotedItem, mentions} mc fInvDescr_ =
|
||||
if isNothing fInvDescr_ && not (msgContentHasText mc)
|
||||
then pure []
|
||||
else do
|
||||
let CIMeta {itemTs, itemSharedMsgId, itemTimed, showGroupAsSender} = meta
|
||||
quotedItemId_ = quoteItemId =<< quotedItem
|
||||
fInv_ = (\(fInv, _, _) -> fInv) <$> fInvDescr_
|
||||
fInv_ = (\(fInv, _, _, _) -> fInv) <$> fInvDescr_
|
||||
(mc', _, mentions') = updatedMentionNames mc formattedText mentions
|
||||
mentions'' = M.map (\CIMention {memberId} -> MsgMention {memberId}) mentions'
|
||||
-- for channel messages default chat version range to membership range
|
||||
@@ -1475,10 +1529,10 @@ sendHistory user gInfo@GroupInfo {membership} m@GroupMember {activeConn = Just c
|
||||
(chatMsgEvent, _) <- withStore $ \db -> prepareGroupMsg db user gInfo Nothing showGroupAsSender mc' mentions'' quotedItemId_ Nothing fInv_ itemTimed False
|
||||
pure $ VMUnsigned ChatMessage {chatVRange = senderVRange, msgId = itemSharedMsgId, chatMsgEvent}
|
||||
fileDescrEvents <- case (fInvDescr_, itemSharedMsgId) of
|
||||
(Just (_, fileDescrText, fileExpires), Just msgId) -> do
|
||||
(Just (_, fileDescrText, fileExpires, descrBadge), Just msgId) -> do
|
||||
partSize <- asks $ xftpDescrPartSize . config
|
||||
let parts = splitFileDescr partSize fileDescrText
|
||||
pure . L.toList $ L.map (\fd -> XMsgFileDescr msgId fd fileExpires) parts
|
||||
let parts = splitFileDescr partSize (maybe partSize (const badgeDescrPartSize) descrBadge) fileDescrText
|
||||
pure . L.toList $ L.map (\fd@FileDescr {fileDescrComplete} -> XMsgFileDescr msgId fd fileExpires (if fileDescrComplete then descrBadge else Nothing)) parts
|
||||
_ -> pure []
|
||||
let fileDescrVMs = map (VMUnsigned . ChatMessage senderVRange Nothing) fileDescrEvents
|
||||
pure $ map ((,) fwd) (contentVM : fileDescrVMs)
|
||||
@@ -1488,11 +1542,16 @@ memberShortenedName GroupMember {memberProfile = LocalProfile {displayName}}
|
||||
| T.length displayName <= 16 = displayName
|
||||
| otherwise = T.take 16 displayName `T.snoc` '…'
|
||||
|
||||
splitFileDescr :: Int -> RcvFileDescrText -> NonEmpty FileDescr
|
||||
splitFileDescr partSize rfdText = splitParts 1 rfdText
|
||||
-- the description proof travels on the last part, so that part leaves room for it
|
||||
badgeDescrPartSize :: Int
|
||||
badgeDescrPartSize = 13500
|
||||
|
||||
splitFileDescr :: Int -> Int -> RcvFileDescrText -> NonEmpty FileDescr
|
||||
splitFileDescr partSize lastSize rfdText = splitParts 1 rfdText
|
||||
where
|
||||
splitParts partNo remText =
|
||||
let (part, rest) = T.splitAt partSize remText
|
||||
let n = T.length remText
|
||||
(part, rest) = T.splitAt (if n <= lastSize then n else if n <= partSize then lastSize else partSize) remText
|
||||
complete = T.null rest
|
||||
fileDescr = FileDescr {fileDescrText = part, fileDescrPartNo = partNo, fileDescrComplete = complete}
|
||||
in if complete
|
||||
@@ -2263,6 +2322,71 @@ groupBindingData gks memberId memberKey = case gks >>= publicGroupKeys of
|
||||
Just PublicGroupKeys {publicGroupId} -> smpEncode (publicGroupId, memberId)
|
||||
Nothing -> smpEncode (memberId, memberKey)
|
||||
|
||||
type HistoryFile = (FileInvitation, RcvFileDescrText, Maybe UTCTime, Maybe BadgeProof)
|
||||
|
||||
directChatBinding :: Contact -> CM (Maybe ByteString)
|
||||
directChatBinding ct =
|
||||
forM (contactConn ct) $ \conn ->
|
||||
encodeChatBinding CBDirect <$> withAgent (`getConnectionRatchetAdHash` aConnId conn)
|
||||
|
||||
rcvGroupChatBinding :: GroupInfo -> Maybe GroupMember -> ShowGroupAsSender -> Maybe BadgeProof -> Maybe ByteString
|
||||
rcvGroupChatBinding GroupInfo {groupKeys} m_ asGroup badge_ =
|
||||
case (groupKeys >>= publicGroupKeys, asGroup, m_) of
|
||||
(Just PublicGroupKeys {publicGroupId}, True, _) ->
|
||||
Just $ encodeChatBinding CBChannel $ smpEncode publicGroupId
|
||||
(Just PublicGroupKeys {publicGroupId}, False, Just GroupMember {memberId}) ->
|
||||
Just $ encodeChatBinding CBGroup $ smpEncode (publicGroupId, memberId)
|
||||
(Nothing, False, Just GroupMember {memberId, memberPubKey}) ->
|
||||
(\k -> encodeChatBinding CBGroup $ smpEncode (memberId, k)) <$> (memberPubKey <|> proofMemberKey memberId badge_)
|
||||
_ -> Nothing
|
||||
|
||||
proofMemberKey :: MemberId -> Maybe BadgeProof -> Maybe C.PublicKeyEd25519
|
||||
proofMemberKey memberId badge_ = do
|
||||
BadgeProof _ (BBSPresHeader phBytes) _ _ <- badge_
|
||||
binding <- headerChatBinding =<< eitherToMaybe (strDecode phBytes)
|
||||
d <- B.stripPrefix (smpEncode CBGroup) binding
|
||||
(mId, k) <- eitherToMaybe (smpDecode d :: Either String (MemberId, C.PublicKeyEd25519))
|
||||
if mId == memberId then Just k else Nothing
|
||||
where
|
||||
headerChatBinding = \case
|
||||
PHFileInv {chatBinding} -> Just chatBinding
|
||||
PHFileDescr {chatBinding} -> Just chatBinding
|
||||
_ -> Nothing
|
||||
|
||||
badgeProofStatus :: Maybe ProofPresHeader -> BadgeProof -> CM BadgeStatus
|
||||
badgeProofStatus expected_ badge@BadgeProof {presHeader = BBSPresHeader phBytes, badgeInfo} =
|
||||
case expected_ of
|
||||
Just expected | phBytes == strEncode expected -> do
|
||||
keys <- asks $ badgePublicKeys . config
|
||||
verified <- liftIO $ verifyBadge keys badge
|
||||
now <- liftIO getCurrentTime
|
||||
pure $ mkBadgeStatus now verified badgeInfo
|
||||
_ -> pure BSFailed
|
||||
|
||||
rcvDirectFileProhibited :: Contact -> FileInvitation -> CM (Maybe FileProhibited)
|
||||
rcvDirectFileProhibited ct fInv@FileInvitation {fileBadge} = do
|
||||
binding_ <- if isJust fileBadge then directChatBinding ct else pure Nothing
|
||||
rcvFileProhibited binding_ fInv
|
||||
|
||||
rcvGroupFileProhibited :: GroupInfo -> Maybe GroupMember -> ShowGroupAsSender -> FileInvitation -> CM (Maybe FileProhibited)
|
||||
rcvGroupFileProhibited gInfo m_ asGroup fInv@FileInvitation {fileBadge} =
|
||||
rcvFileProhibited (rcvGroupChatBinding gInfo m_ asGroup fileBadge) fInv
|
||||
|
||||
rcvFileProhibited :: Maybe ByteString -> FileInvitation -> CM (Maybe FileProhibited)
|
||||
rcvFileProhibited binding_ FileInvitation {fileSize, fileBadge} = do
|
||||
lims <- asks $ fileSizeLimits . config
|
||||
if fileSize <= noBadge lims
|
||||
then pure Nothing
|
||||
else case fileBadge of
|
||||
Nothing -> pure $ Just FileProhibited {maxSize = noBadge lims, badgeStatus = Nothing}
|
||||
Just badge -> do
|
||||
st <- badgeProofStatus ((\chatBinding -> PHFileInv {chatBinding, fileSize = fromInteger fileSize}) <$> binding_) badge
|
||||
let maxSize = maxXFTPFileSize lims $ Just $ PeerBadge badge st
|
||||
pure $
|
||||
if fileSize <= maxSize
|
||||
then Nothing
|
||||
else Just FileProhibited {maxSize, badgeStatus = Just st}
|
||||
|
||||
createUserMemberKey :: GroupInfo -> CM GroupInfo
|
||||
createUserMemberKey gInfo@GroupInfo {groupId, membership, groupKeys}
|
||||
| useRelays' gInfo || isJust groupKeys = pure gInfo
|
||||
|
||||
@@ -46,6 +46,7 @@ import Data.Time.Format (defaultTimeLocale, formatTime)
|
||||
import qualified Data.UUID as UUID
|
||||
import qualified Data.UUID.V4 as V4
|
||||
import Data.Word (Word32)
|
||||
import Simplex.Chat.Badges (BadgeProof, BadgeProofKind (..), BadgeStatus (..), FileSizeLimits (..), ProofPresHeader (..))
|
||||
import Simplex.Chat.Call
|
||||
import Simplex.Chat.Controller
|
||||
import Simplex.Chat.Delivery
|
||||
@@ -76,7 +77,7 @@ import Simplex.Chat.Types.Preferences
|
||||
import Simplex.Chat.Types.Shared
|
||||
import Simplex.FileTransfer.Description (ValidFileDescription)
|
||||
import qualified Simplex.FileTransfer.Description as FD
|
||||
import Simplex.FileTransfer.Protocol (FilePartyI, GrantedStorageTime (..))
|
||||
import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI, GrantedStorageTime (..))
|
||||
import qualified Simplex.FileTransfer.Transport as XFTP
|
||||
import Simplex.FileTransfer.Types (FileErrorType (..), RcvFileId, SndFileId)
|
||||
import Simplex.Messaging.Agent
|
||||
@@ -224,7 +225,7 @@ processAgentMsgSndFile _corrId aFileId msg = do
|
||||
-- we have 1 chunk - use it as URI whether it is redirect or not
|
||||
ft' <- maybe (pure ft) (\fId -> withStore $ \db -> getFileTransferMeta db user fId) xftpRedirectFor
|
||||
toView $ CEvtSndStandaloneFileComplete user ft' $ map (decodeLatin1 . strEncode . FD.fileDescriptionURI) rfds'
|
||||
Just (AChatItem _ d cInfo _ci@ChatItem {meta = CIMeta {itemSharedMsgId = msgId_, itemDeleted}}) ->
|
||||
Just (AChatItem _ d cInfo _ci@ChatItem {meta = CIMeta {itemSharedMsgId = msgId_, itemDeleted, showGroupAsSender}}) ->
|
||||
case (msgId_, itemDeleted) of
|
||||
(Just sharedMsgId, Nothing) -> do
|
||||
when (length rfds < length sfts) $ throwChatError $ CEInternalError "not enough XFTP file descriptions to send"
|
||||
@@ -232,9 +233,16 @@ processAgentMsgSndFile _corrId aFileId msg = do
|
||||
toView $ CEvtSndFileProgressXFTP user ci ft 1 1
|
||||
case (rfds, sfts, d, cInfo) of
|
||||
(rfd : extraRFDs, sft : _, SMDSnd, DirectChat ct) -> do
|
||||
withStore' $ \db -> createExtraSndFTDescrs db user fileId (map fileDescrText extraRFDs)
|
||||
conn@Connection {connId} <- liftEither $ contactSendConn_ ct
|
||||
sendFileDescriptions (ConnectionId connId) ((conn, sft, fileDescrText rfd) :| []) sharedMsgId fileExpires >>= \case
|
||||
let FileTransferMeta {fileSize} = ft
|
||||
binding_ <- ifM ((not (contactConnIncognito ct) &&) <$> fileNeedsBadge fileSize) (directChatBinding ct) (pure Nothing)
|
||||
descrBadge <- pure binding_ $>>= \chatBinding ->
|
||||
let FD.ValidFileDescription fd = sndDescr
|
||||
in sndBadgeProof user PHFileDescr {chatBinding, fileSize = fromInteger fileSize, descrHash = FD.sharedDescriptionHash fd, fileExpires}
|
||||
withStore' $ \db -> do
|
||||
createExtraSndFTDescrs db user fileId (map fileDescrText extraRFDs)
|
||||
forM_ descrBadge $ createFileBadgeProof db fileId BPKDescription
|
||||
sendFileDescriptions (ConnectionId connId) ((conn, sft, fileDescrText rfd) :| []) sharedMsgId fileExpires descrBadge >>= \case
|
||||
Just rs -> case L.last rs of
|
||||
Right ([msgDeliveryId], _) ->
|
||||
withStore' $ \db -> updateSndFTDeliveryXFTP db sft msgDeliveryId
|
||||
@@ -247,9 +255,17 @@ processAgentMsgSndFile _corrId aFileId msg = do
|
||||
ms <- getRecipients
|
||||
let rfdsMemberFTs = zipWith (\rfd (conn, sft) -> (conn, sft, fileDescrText rfd)) rfds (memberFTs ms)
|
||||
extraRFDs = drop (length rfdsMemberFTs) rfds
|
||||
withStore' $ \db -> createExtraSndFTDescrs db user fileId (map fileDescrText extraRFDs)
|
||||
FileTransferMeta {fileSize} = ft
|
||||
needsBadge <- fileNeedsBadge fileSize
|
||||
let binding_ = if needsBadge && not (incognitoMembership g) then sndGroupChatBinding g showGroupAsSender else Nothing
|
||||
descrBadge <- pure binding_ $>>= \chatBinding ->
|
||||
let FD.ValidFileDescription fd = sndDescr
|
||||
in sndBadgeProof user PHFileDescr {chatBinding, fileSize = fromInteger fileSize, descrHash = FD.sharedDescriptionHash fd, fileExpires}
|
||||
withStore' $ \db -> do
|
||||
createExtraSndFTDescrs db user fileId (map fileDescrText extraRFDs)
|
||||
forM_ descrBadge $ createFileBadgeProof db fileId BPKDescription
|
||||
forM_ (L.nonEmpty rfdsMemberFTs) $ \rfdsMemberFTs' ->
|
||||
sendFileDescriptions (GroupId groupId) rfdsMemberFTs' sharedMsgId fileExpires
|
||||
sendFileDescriptions (GroupId groupId) rfdsMemberFTs' sharedMsgId fileExpires descrBadge
|
||||
ci' <- withStore $ \db -> do
|
||||
liftIO $ updateCIFileStatus db user fileId CIFSSndComplete
|
||||
getChatItemByFileId db cxt user fileId
|
||||
@@ -278,8 +294,8 @@ processAgentMsgSndFile _corrId aFileId msg = do
|
||||
where
|
||||
fileDescrText :: FilePartyI p => ValidFileDescription p -> T.Text
|
||||
fileDescrText = safeDecodeUtf8 . strEncode
|
||||
sendFileDescriptions :: ConnOrGroupId -> NonEmpty (Connection, SndFileTransfer, RcvFileDescrText) -> SharedMsgId -> Maybe UTCTime -> CM (Maybe (NonEmpty (Either ChatError ([Int64], PQEncryption))))
|
||||
sendFileDescriptions connOrGroupId connsTransfersDescrs sharedMsgId fileExpires = do
|
||||
sendFileDescriptions :: ConnOrGroupId -> NonEmpty (Connection, SndFileTransfer, RcvFileDescrText) -> SharedMsgId -> Maybe UTCTime -> Maybe BadgeProof -> CM (Maybe (NonEmpty (Either ChatError ([Int64], PQEncryption))))
|
||||
sendFileDescriptions connOrGroupId connsTransfersDescrs sharedMsgId fileExpires descrBadge = do
|
||||
lift . void . withStoreBatch' $ \db -> L.map (\(_, sft, rfdText) -> updateSndFTDescrXFTP db user sft rfdText) connsTransfersDescrs
|
||||
partSize <- asks $ xftpDescrPartSize . config
|
||||
let connsIdsEvts = connDescrEvents partSize
|
||||
@@ -295,7 +311,7 @@ processAgentMsgSndFile _corrId aFileId msg = do
|
||||
where
|
||||
splitText :: (Connection, SndFileTransfer, RcvFileDescrText) -> [(Connection, (ConnOrGroupId, Maybe MsgSigning, ChatMsgEvent 'Json))]
|
||||
splitText (conn, _, rfdText) =
|
||||
map (\fileDescr -> (conn, (connOrGroupId, Nothing, XMsgFileDescr {msgId = sharedMsgId, fileDescr, fileExpires}))) (L.toList $ splitFileDescr partSize rfdText)
|
||||
map (\fileDescr@FileDescr {fileDescrComplete} -> (conn, (connOrGroupId, Nothing, XMsgFileDescr {msgId = sharedMsgId, fileDescr, fileExpires, fileBadge = if fileDescrComplete then descrBadge else Nothing}))) (L.toList $ splitFileDescr partSize (maybe partSize (const badgeDescrPartSize) descrBadge) rfdText)
|
||||
toMsgReq :: (Connection, (ConnOrGroupId, Maybe MsgSigning, ChatMsgEvent 'Json)) -> SndMessage -> ChatMsgReq
|
||||
toMsgReq (conn, _) SndMessage {msgId, msgBody} =
|
||||
(conn, MsgFlags {notification = hasNotification XMsgFileDescr_}, (vrValue msgBody, [msgId]))
|
||||
@@ -550,7 +566,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
let ct'' = ct' {activeConn = Just conn''} :: Contact
|
||||
case event of
|
||||
XMsgNew mc -> newContentMessage ct'' mc msg msgMeta
|
||||
XMsgFileDescr sharedMsgId fileDescr fileExpires -> messageFileDescription ct'' sharedMsgId fileDescr fileExpires
|
||||
XMsgFileDescr sharedMsgId fileDescr fileExpires fileBadge -> messageFileDescription ct'' sharedMsgId fileDescr fileExpires fileBadge
|
||||
XMsgUpdate sharedMsgId mContent _ ttl live _msgScope _ -> messageUpdate ct'' sharedMsgId mContent msg msgMeta ttl live
|
||||
XMsgDel sharedMsgId _ _ _ -> messageDelete ct'' sharedMsgId msg msgMeta
|
||||
XMsgReact sharedMsgId _ _ reaction add -> directMsgReaction ct'' sharedMsgId reaction add msg msgMeta
|
||||
@@ -1040,7 +1056,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
where
|
||||
MsgContainer {scope, asGroup} = mc
|
||||
-- file description is always allowed, to allow sending files to support scope
|
||||
XMsgFileDescr sharedMsgId fileDescr fileExpires -> groupMessageFileDescription gInfo' (Just m'') sharedMsgId fileDescr fileExpires
|
||||
XMsgFileDescr sharedMsgId fileDescr fileExpires fileBadge -> groupMessageFileDescription gInfo' (Just m'') sharedMsgId fileDescr fileExpires fileBadge
|
||||
XMsgUpdate sharedMsgId mContent mentions ttl live msgScope asGroup_ ->
|
||||
checkSendAsGroup asGroup_ $
|
||||
memberCanSend (Just m'') msgScope $
|
||||
@@ -1896,7 +1912,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
let MsgContainer {ttl = itemTTL, live = live_} = mc
|
||||
timed_ = rcvContactCITimed ct itemTTL
|
||||
live = fromMaybe False live_
|
||||
file_ <- processFileInvitation fInv_ content $ \db -> createRcvFileTransfer db userId ct
|
||||
file_ <- processFileInvitation fInv_ content (rcvDirectFileProhibited ct) $ \db -> createRcvFileTransfer db userId ct
|
||||
newChatItem (CIRcvMsgContent content, msgContentTexts content) (snd <$> file_) timed_ live
|
||||
autoAcceptFile file_
|
||||
where
|
||||
@@ -1912,36 +1928,37 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
ChatConfig {autoAcceptFileSize = sz} <- asks config
|
||||
when (sz > fileSize) $ receiveFileEvt' user ft False Nothing Nothing >>= toView
|
||||
|
||||
messageFileDescription :: Contact -> SharedMsgId -> FileDescr -> Maybe UTCTime -> CM ()
|
||||
messageFileDescription Contact {contactId} sharedMsgId fileDescr fileExpires = do
|
||||
messageFileDescription :: Contact -> SharedMsgId -> FileDescr -> Maybe UTCTime -> Maybe BadgeProof -> CM ()
|
||||
messageFileDescription ct@Contact {contactId} sharedMsgId fileDescr fileExpires fileBadge = do
|
||||
(fileId, aci) <- withStore $ \db -> do
|
||||
fileId <- getFileIdBySharedMsgId db userId contactId sharedMsgId
|
||||
aci <- getChatItemByFileId db cxt user fileId
|
||||
pure (fileId, aci)
|
||||
processFDMessage fileId aci fileDescr fileExpires
|
||||
binding_ <- if isJust fileBadge then directChatBinding ct else pure Nothing
|
||||
processFDMessage binding_ fileId aci fileDescr fileExpires fileBadge
|
||||
|
||||
groupMessageFileDescription :: GroupInfo -> Maybe GroupMember -> SharedMsgId -> FileDescr -> Maybe UTCTime -> CM (Maybe DeliveryTaskContext)
|
||||
groupMessageFileDescription g@GroupInfo {groupId} m_ sharedMsgId fileDescr fileExpires = do
|
||||
groupMessageFileDescription :: GroupInfo -> Maybe GroupMember -> SharedMsgId -> FileDescr -> Maybe UTCTime -> Maybe BadgeProof -> CM (Maybe DeliveryTaskContext)
|
||||
groupMessageFileDescription g@GroupInfo {groupId} m_ sharedMsgId fileDescr fileExpires fileBadge = do
|
||||
(fileId, aci) <- withStore $ \db -> do
|
||||
fileId <- getGroupFileIdBySharedMsgId db userId groupId sharedMsgId
|
||||
aci <- getChatItemByFileId db cxt user fileId
|
||||
pure (fileId, aci)
|
||||
case aci of
|
||||
AChatItem SCTGroup SMDRcv (GroupChat _g scopeInfo) ChatItem {chatDir}
|
||||
AChatItem SCTGroup SMDRcv (GroupChat _g scopeInfo) ChatItem {chatDir, meta = CIMeta {showGroupAsSender}}
|
||||
| validSender m_ chatDir -> do
|
||||
-- in processFDMessage some paths are programmed as errors,
|
||||
-- for example failure on not approved relays (CEFileNotApproved).
|
||||
-- we catch error, so that even if processFDMessage fails, message can still be forwarded.
|
||||
processFDMessage fileId aci fileDescr fileExpires `catchAllErrors` \_ -> pure ()
|
||||
processFDMessage (rcvGroupChatBinding g m_ showGroupAsSender fileBadge) fileId aci fileDescr fileExpires fileBadge `catchAllErrors` \_ -> pure ()
|
||||
pure $ Just $ infoToDeliveryContext g scopeInfo (isChannelDir chatDir)
|
||||
| otherwise -> messageError "x.msg.file.descr: file/sender mismatch" $> Nothing
|
||||
_ -> messageError "x.msg.file.descr: invalid file description part" $> Nothing
|
||||
|
||||
processFDMessage :: FileTransferId -> AChatItem -> FileDescr -> Maybe UTCTime -> CM ()
|
||||
processFDMessage fileId aci fileDescr fileExpires = do
|
||||
processFDMessage :: Maybe ByteString -> FileTransferId -> AChatItem -> FileDescr -> Maybe UTCTime -> Maybe BadgeProof -> CM ()
|
||||
processFDMessage binding_ fileId aci fileDescr fileExpires fileBadge = do
|
||||
ft <- withStore $ \db -> getRcvFileTransfer db user fileId
|
||||
unless (rcvFileCompleteOrCancelled ft) $ do
|
||||
(rfd@RcvFileDescr {fileDescrComplete}, ft'@RcvFileTransfer {fileStatus, xftpRcvFile, cryptoArgs, fileInvitation = FileInvitation {fileSize}}) <- withStore $ \db -> do
|
||||
(rfd@RcvFileDescr {fileDescrComplete}, ft'@RcvFileTransfer {fileStatus, xftpRcvFile, cryptoArgs, fileProhibited, fileInvitation = FileInvitation {fileSize}}) <- withStore $ \db -> do
|
||||
rfd <- appendRcvFD db userId fileId fileDescr
|
||||
forM_ fileExpires $ liftIO . setFileExpiration db user fileId
|
||||
-- reading second time in the same transaction as appending description
|
||||
@@ -1949,16 +1966,41 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
ft' <- getRcvFileTransfer db user fileId
|
||||
pure (rfd, ft')
|
||||
when fileDescrComplete $ toView $ CEvtRcvFileDescrReady user aci ft' rfd
|
||||
case (fileStatus, xftpRcvFile) of
|
||||
(RFSAccepted _, Just XFTPRcvFile {userApprovedRelays}) -> receiveViaCompleteFD user fileId rfd fileSize userApprovedRelays cryptoArgs
|
||||
_ -> pure ()
|
||||
maxSize <- asks $ noBadge . fileSizeLimits . config
|
||||
let descrBadgeRequired = fileDescrComplete && fileSize > maxSize && isNothing fileProhibited
|
||||
prohibited_ <- case fileBadge of
|
||||
_ | not descrBadgeRequired -> pure Nothing
|
||||
Nothing -> pure $ Just FileProhibited {maxSize, badgeStatus = Nothing}
|
||||
Just badge -> do
|
||||
st <- descrBadgeStatus binding_ fileSize rfd fileExpires badge
|
||||
if st == BSActive
|
||||
then do
|
||||
withStore' $ \db -> createFileBadgeProof db fileId BPKDescription badge
|
||||
pure Nothing
|
||||
else pure $ Just FileProhibited {maxSize, badgeStatus = Just st}
|
||||
case prohibited_ of
|
||||
Nothing -> case (fileStatus, xftpRcvFile) of
|
||||
(RFSAccepted _, Just XFTPRcvFile {userApprovedRelays}) -> receiveViaCompleteFD user fileId rfd fileSize userApprovedRelays cryptoArgs
|
||||
_ -> pure ()
|
||||
-- the file may already be accepted, so it is reset to an invitation the apps refuse by its prohibition
|
||||
Just prohibited -> do
|
||||
withStore' $ \db -> setFileProhibited db user fileId prohibited
|
||||
aci_ <- resetRcvCIFileStatus user fileId CIFSRcvInvitation
|
||||
forM_ aci_ $ \aci' -> toView $ CEvtChatItemUpdated user aci'
|
||||
|
||||
processFileInvitation :: Maybe FileInvitation -> MsgContent -> (DB.Connection -> FileInvitation -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer) -> CM (Maybe (RcvFileTransfer, CIFile 'MDRcv))
|
||||
processFileInvitation fInv_ mc createRcvFT = forM fInv_ $ \fInv -> do
|
||||
descrBadgeStatus :: Maybe ByteString -> Integer -> RcvFileDescr -> Maybe UTCTime -> BadgeProof -> CM BadgeStatus
|
||||
descrBadgeStatus binding_ fileSize RcvFileDescr {fileDescrText} fileExpires badge = do
|
||||
FD.ValidFileDescription fd <- parseFileDescription @'FRecipient fileDescrText
|
||||
let descrHash = FD.sharedDescriptionHash fd
|
||||
badgeProofStatus ((\chatBinding -> PHFileDescr {chatBinding, fileSize = fromInteger fileSize, descrHash, fileExpires}) <$> binding_) badge
|
||||
|
||||
processFileInvitation :: Maybe FileInvitation -> MsgContent -> (FileInvitation -> CM (Maybe FileProhibited)) -> (DB.Connection -> FileInvitation -> Maybe FileProhibited -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer) -> CM (Maybe (RcvFileTransfer, CIFile 'MDRcv))
|
||||
processFileInvitation fInv_ mc fileProhibited_ createRcvFT = forM fInv_ $ \fInv -> do
|
||||
ChatConfig {fileChunkSize} <- asks config
|
||||
fInv'@FileInvitation {fileName, fileSize} <- validateFileInvitation fInv
|
||||
fileProhibited <- fileProhibited_ fInv'
|
||||
inline <- receiveInlineMode fInv' (Just mc) fileChunkSize
|
||||
ft@RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFT db fInv' inline fileChunkSize
|
||||
ft@RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFT db fInv' fileProhibited inline fileChunkSize
|
||||
let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP
|
||||
(filePath, fileStatus, ft') <- case inline of
|
||||
Just IFMSent -> do
|
||||
@@ -1970,15 +2012,19 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
_ -> pure (Nothing, CIFSRcvInvitation, ft)
|
||||
let RcvFileTransfer {cryptoArgs} = ft'
|
||||
fileSource = (`CryptoFile` cryptoArgs) <$> filePath
|
||||
pure (ft', CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol, fileExpires = Nothing})
|
||||
pure (ft', CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol, fileExpires = Nothing, fileProhibited})
|
||||
|
||||
mkValidFileInvitation :: FileInvitation -> FileInvitation
|
||||
mkValidFileInvitation fInv@FileInvitation {fileName} = fInv {fileName = safeFileNameStr fileName}
|
||||
|
||||
validateFileInvitation :: FileInvitation -> CM FileInvitation
|
||||
validateFileInvitation fInv@FileInvitation {fileName, fileSize}
|
||||
| fileSize > 0 = pure $ mkValidFileInvitation fInv
|
||||
| otherwise = throwChatError $ CEFileSize fileName
|
||||
validateFileInvitation fInv@FileInvitation {fileName, fileSize, fileDescr}
|
||||
| fileSize <= 0 = throwChatError $ CEFileSize fileName
|
||||
| otherwise = do
|
||||
-- a file that requires a badge is received from the description message, where the proof binds the description
|
||||
needsBadge <- fileNeedsBadge fileSize
|
||||
let fileDescr' = if needsBadge then dummyFileDescr <$ fileDescr else fileDescr
|
||||
pure $ mkValidFileInvitation fInv {fileDescr = fileDescr'}
|
||||
|
||||
messageUpdate :: Contact -> SharedMsgId -> MsgContent -> RcvMessage -> MsgMeta -> Maybe Int -> Maybe Bool -> CM ()
|
||||
messageUpdate ct@Contact {contactId} sharedMsgId mc msg@RcvMessage {msgId} msgMeta ttl live_ = do
|
||||
@@ -2201,7 +2247,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
unless (maybe False memberBlocked m') $ autoAcceptFile file_
|
||||
processFileInv gInfo' m' =
|
||||
let fileMember_ = if sentAsGroup then Nothing else m'
|
||||
in processFileInvitation fInv_ content $ \db -> createRcvGroupFileTransfer db userId gInfo' fileMember_ FTNormal sharedMsgId_
|
||||
in processFileInvitation fInv_ content (rcvGroupFileProhibited gInfo' m' sentAsGroup) $ \db -> createRcvGroupFileTransfer db userId gInfo' fileMember_ FTNormal sharedMsgId_
|
||||
newChatItem gInfo' m' scopeInfo ciContent ciFile_ timed live = do
|
||||
let mentions' = if maybe False memberBlocked m' then M.empty else mentions
|
||||
(ci, cInfo) <- saveRcvCI gInfo' m' scopeInfo ciContent ciFile_ timed live mentions'
|
||||
@@ -2423,10 +2469,11 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
processFileInvitation' ct fInv msg@RcvMessage {sharedMsgId_} msgMeta = do
|
||||
ChatConfig {fileChunkSize} <- asks config
|
||||
fInv'@FileInvitation {fileName, fileSize} <- validateFileInvitation fInv
|
||||
fileProhibited <- rcvDirectFileProhibited ct fInv'
|
||||
inline <- receiveInlineMode fInv' Nothing fileChunkSize
|
||||
RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFileTransfer db userId ct fInv' inline fileChunkSize
|
||||
RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFileTransfer db userId ct fInv' fileProhibited inline fileChunkSize
|
||||
let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP
|
||||
ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol, fileExpires = Nothing}
|
||||
ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol, fileExpires = Nothing, fileProhibited}
|
||||
content = ciContentNoParse $ CIRcvMsgContent $ MCFile ""
|
||||
(ci, cInfo) <- saveRcvChatItem' user (CDDirectRcv ct) msg sharedMsgId_ brokerTs content ciFile Nothing False M.empty
|
||||
toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv cInfo ci]
|
||||
@@ -2438,10 +2485,11 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
processGroupFileInvitation' gInfo m fInv msg@RcvMessage {sharedMsgId_} brokerTs = do
|
||||
ChatConfig {fileChunkSize} <- asks config
|
||||
fInv'@FileInvitation {fileName, fileSize} <- validateFileInvitation fInv
|
||||
fileProhibited <- rcvGroupFileProhibited gInfo (Just m) False fInv'
|
||||
inline <- receiveInlineMode fInv' Nothing fileChunkSize
|
||||
RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvGroupFileTransfer db userId gInfo (Just m) FTNormal sharedMsgId_ fInv' inline fileChunkSize
|
||||
RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvGroupFileTransfer db userId gInfo (Just m) FTNormal sharedMsgId_ fInv' fileProhibited inline fileChunkSize
|
||||
let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP
|
||||
ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol, fileExpires = Nothing}
|
||||
ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol, fileExpires = Nothing, fileProhibited}
|
||||
content = ciContentNoParse $ CIRcvMsgContent $ MCFile ""
|
||||
(ci, cInfo) <- saveRcvChatItem' user (CDGroupRcv gInfo Nothing m) msg sharedMsgId_ brokerTs content ciFile Nothing False M.empty
|
||||
ci' <- blockedMemberCI gInfo m ci
|
||||
@@ -3410,7 +3458,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
cleanupRosterTransfer gInfo (groupMemberId' fromMember)
|
||||
let relayHdr = if isUserGrpFwdRelay gInfo then Just sm else Nothing
|
||||
chSize <- asks $ fileChunkSize . config
|
||||
let rosterFInv = FileInvitation {fileName = "roster", fileSize, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Just IFMSent, fileDescr = Nothing}
|
||||
let rosterFInv = FileInvitation {fileName = "roster", fileSize, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Just IFMSent, fileDescr = Nothing, fileBadge = Nothing}
|
||||
-- transfer record + its scratch file in one transaction (file owned by the transfer, keyed per source)
|
||||
rft@RcvFileTransfer {fileId} <- withStore $ \db -> do
|
||||
transferId <- liftIO $ createRosterTransfer db gInfo (groupMemberId' fromMember) newVer fileDigest (groupMemberId' author) brokerTs relayHdr
|
||||
@@ -3899,7 +3947,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
where
|
||||
MsgContainer {scope} = mc
|
||||
-- file description is always allowed, to allow sending files to support scope
|
||||
XMsgFileDescr sharedMsgId fileDescr fileExpires -> void $ groupMessageFileDescription gInfo author_ sharedMsgId fileDescr fileExpires
|
||||
XMsgFileDescr sharedMsgId fileDescr fileExpires fileBadge -> void $ groupMessageFileDescription gInfo author_ sharedMsgId fileDescr fileExpires fileBadge
|
||||
XMsgUpdate sharedMsgId mContent mentions ttl live msgScope asGroup_ ->
|
||||
void $ memberCanSend author_ msgScope $ groupMessageUpdate gInfo author_ sharedMsgId mContent mentions msgScope rcvMsg msgTs ttl live asGroup_
|
||||
XMsgDel sharedMsgId memId scope_ _ -> void $ groupMessageDelete gInfo author_ sharedMsgId memId scope_ False rcvMsg msgTs
|
||||
|
||||
@@ -688,7 +688,8 @@ data CIFile (d :: MsgDirection) = CIFile
|
||||
fileSource :: Maybe CryptoFile, -- local file path with optional key and nonce
|
||||
fileStatus :: CIFileStatus d,
|
||||
fileProtocol :: FileProtocol,
|
||||
fileExpires :: Maybe UTCTime
|
||||
fileExpires :: Maybe UTCTime,
|
||||
fileProhibited :: Maybe FileProhibited
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ import Data.Time.Clock.System (systemToUTCTime, utcToSystemTime)
|
||||
import Data.Type.Equality
|
||||
import Data.Typeable (Typeable)
|
||||
import Data.Word (Word16, Word32)
|
||||
import Simplex.Chat.Badges (LocalBadge)
|
||||
import Simplex.Chat.Badges (BadgeProof, LocalBadge)
|
||||
import Simplex.Chat.Call
|
||||
import Simplex.Chat.Options.DB (FromField (..), ToField (..))
|
||||
import Simplex.Chat.Types
|
||||
@@ -451,7 +451,7 @@ signChatMsgBody MsgSigning {bindingTag, bindingData, keyRef, privKey} msgBody =
|
||||
|
||||
data ChatMsgEvent (e :: MsgEncoding) where
|
||||
XMsgNew :: MsgContainer -> ChatMsgEvent 'Json
|
||||
XMsgFileDescr :: {msgId :: SharedMsgId, fileDescr :: FileDescr, fileExpires :: Maybe UTCTime} -> ChatMsgEvent 'Json
|
||||
XMsgFileDescr :: {msgId :: SharedMsgId, fileDescr :: FileDescr, fileExpires :: Maybe UTCTime, fileBadge :: Maybe BadgeProof} -> ChatMsgEvent 'Json
|
||||
XMsgUpdate :: {msgId :: SharedMsgId, content :: MsgContent, mentions :: Map MemberName MsgMention, ttl :: Maybe Int, live :: Maybe Bool, scope :: Maybe MsgScope, asGroup :: Maybe Bool} -> ChatMsgEvent 'Json
|
||||
XMsgDel :: {msgId :: SharedMsgId, memberId :: Maybe MemberId, scope :: Maybe MsgScope, onlyHistory :: Bool} -> ChatMsgEvent 'Json
|
||||
XMsgDeleted :: ChatMsgEvent 'Json
|
||||
@@ -1397,7 +1397,7 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do
|
||||
msg :: CMEventTag 'Json -> Either String (ChatMsgEvent 'Json)
|
||||
msg = \case
|
||||
XMsgNew_ -> XMsgNew <$> JT.parseEither parseJSON (J.Object params)
|
||||
XMsgFileDescr_ -> XMsgFileDescr <$> p "msgId" <*> p "fileDescr" <*> opt "fileExpires"
|
||||
XMsgFileDescr_ -> XMsgFileDescr <$> p "msgId" <*> p "fileDescr" <*> opt "fileExpires" <*> opt "fileBadge"
|
||||
XMsgUpdate_ -> do
|
||||
msgId' <- p "msgId"
|
||||
content <- p "content"
|
||||
@@ -1489,7 +1489,7 @@ chatToAppMessage chatMsg@ChatMessage {chatVRange, msgId, chatMsgEvent} = case en
|
||||
XMsgNew mc -> case toJSON mc of
|
||||
J.Object obj -> obj
|
||||
_ -> JM.empty
|
||||
XMsgFileDescr msgId' fileDescr fileExpires -> o $ ("fileExpires" .=? fileExpires) ["msgId" .= msgId', "fileDescr" .= fileDescr]
|
||||
XMsgFileDescr msgId' fileDescr fileExpires fileBadge -> o $ ("fileExpires" .=? fileExpires) $ ("fileBadge" .=? fileBadge) ["msgId" .= msgId', "fileDescr" .= fileDescr]
|
||||
XMsgUpdate {msgId = msgId', content, mentions, ttl, live, scope, asGroup} -> o $ ("asGroup" .=? asGroup) $ ("ttl" .=? ttl) $ ("live" .=? live) $ ("scope" .=? scope) $ ("mentions" .=? nonEmptyMap mentions) ["msgId" .= msgId', "content" .= content]
|
||||
XMsgDel msgId' memberId scope onlyHistory -> o $ ("memberId" .=? memberId) $ ("scope" .=? scope) $ ("onlyHistory" .=? justTrue onlyHistory) ["msgId" .= msgId']
|
||||
XMsgDeleted -> JM.empty
|
||||
|
||||
@@ -45,6 +45,9 @@ module Simplex.Chat.Store.Files
|
||||
updateSndFileStatus,
|
||||
createRcvFileTransfer,
|
||||
createRcvGroupFileTransfer,
|
||||
createFileBadgeProof,
|
||||
getFileBadgeProofs,
|
||||
setFileProhibited,
|
||||
createRosterRcvFile,
|
||||
createRcvStandaloneFileTransfer,
|
||||
appendRcvFD,
|
||||
@@ -86,6 +89,7 @@ import Control.Monad.IO.Class
|
||||
import Data.Either (rights)
|
||||
import Data.Functor ((<&>))
|
||||
import Data.Int (Int64)
|
||||
import Data.List (foldl')
|
||||
import Data.Maybe (fromMaybe, isJust, listToMaybe)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
@@ -93,6 +97,7 @@ import Data.Time (addUTCTime)
|
||||
import Data.Time.Clock (UTCTime (..), getCurrentTime, nominalDay)
|
||||
import Data.Type.Equality
|
||||
import Data.Word (Word32)
|
||||
import Simplex.Chat.Badges (BadgeProof, BadgeProofKind (..), BadgeProofRow, BadgeStatus (..), badgeProofToRow, rowToBadgeProof)
|
||||
import Simplex.Chat.Messages
|
||||
import Simplex.Chat.Messages.CIContent
|
||||
import Simplex.Chat.Store.Messages
|
||||
@@ -180,7 +185,7 @@ getSndFTViaMsgDelivery db User {userId} Connection {connId, agentConnId} agentMs
|
||||
<$> (contactName_ <|> memberName_)
|
||||
|
||||
createSndFileTransferXFTP :: DB.Connection -> User -> Maybe ContactOrGroup -> CryptoFile -> FileInvitation -> AgentSndFileId -> Maybe FileTransferId -> Integer -> IO FileTransferMeta
|
||||
createSndFileTransferXFTP db User {userId} contactOrGroup_ (CryptoFile filePath cryptoArgs) FileInvitation {fileName, fileSize} agentSndFileId xftpRedirectFor chunkSize = do
|
||||
createSndFileTransferXFTP db User {userId} contactOrGroup_ (CryptoFile filePath cryptoArgs) FileInvitation {fileName, fileSize, fileBadge} agentSndFileId xftpRedirectFor chunkSize = do
|
||||
currentTs <- getCurrentTime
|
||||
let xftpSndFile = Just XFTPSndFile {agentSndFileId, privateSndFileDescr = Nothing, agentSndFileDeleted = False, cryptoArgs}
|
||||
DB.execute
|
||||
@@ -188,6 +193,7 @@ createSndFileTransferXFTP db User {userId} contactOrGroup_ (CryptoFile filePath
|
||||
"INSERT INTO files (contact_id, group_id, user_id, file_name, file_path, file_crypto_key, file_crypto_nonce, file_size, chunk_size, redirect_file_id, agent_snd_file_id, ci_file_status, protocol, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
|
||||
(maybe (Nothing, Nothing) contactAndGroupIds contactOrGroup_ :. (userId, fileName, filePath, CF.fileKey <$> cryptoArgs, CF.fileNonce <$> cryptoArgs, fileSize, chunkSize) :. (xftpRedirectFor, agentSndFileId, CIFSSndStored, FPXFTP, currentTs, currentTs))
|
||||
fileId <- insertedRowId db
|
||||
forM_ fileBadge $ createFileBadgeProof db fileId BPKInvitation
|
||||
pure FileTransferMeta {fileId, xftpSndFile, xftpRedirectFor, fileName, filePath, fileSize, fileInline = Nothing, chunkSize, cancelled = False}
|
||||
|
||||
createSndFTDescrXFTP :: DB.Connection -> User -> Maybe GroupMember -> Connection -> FileTransferMeta -> FileDescr -> IO ()
|
||||
@@ -445,8 +451,8 @@ updateSndFileStatus db SndFileTransfer {fileId, connId} status = do
|
||||
currentTs <- getCurrentTime
|
||||
DB.execute db "UPDATE snd_files SET file_status = ?, updated_at = ? WHERE file_id = ? AND connection_id = ?" (status, currentTs, fileId, connId)
|
||||
|
||||
createRcvFileTransfer :: DB.Connection -> UserId -> Contact -> FileInvitation -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer
|
||||
createRcvFileTransfer db userId Contact {contactId, localDisplayName = c} f@FileInvitation {fileName, fileSize, fileConnReq, fileInline, fileDescr} rcvFileInline chunkSize = do
|
||||
createRcvFileTransfer :: DB.Connection -> UserId -> Contact -> FileInvitation -> Maybe FileProhibited -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer
|
||||
createRcvFileTransfer db userId Contact {contactId, localDisplayName = c} f@FileInvitation {fileName, fileSize, fileConnReq, fileInline, fileDescr, fileBadge} prohibited_ rcvFileInline chunkSize = do
|
||||
currentTs <- liftIO getCurrentTime
|
||||
rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr
|
||||
let rfdId = (\RcvFileDescr {fileDescrId} -> fileDescrId) <$> rfd_
|
||||
@@ -456,18 +462,57 @@ createRcvFileTransfer db userId Contact {contactId, localDisplayName = c} f@File
|
||||
fileId <- liftIO $ do
|
||||
DB.execute
|
||||
db
|
||||
"INSERT INTO files (user_id, contact_id, file_name, file_size, chunk_size, file_inline, ci_file_status, protocol, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)"
|
||||
(userId, contactId, fileName, fileSize, chunkSize, fileInline, CIFSRcvInvitation, fileProtocol, currentTs, currentTs)
|
||||
"INSERT INTO files (user_id, contact_id, file_name, file_size, chunk_size, file_inline, ci_file_status, protocol, file_max_size, file_badge_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)"
|
||||
((userId, contactId, fileName, fileSize, chunkSize, fileInline, CIFSRcvInvitation, fileProtocol) :. prohibitedRow prohibited_ :. (currentTs, currentTs))
|
||||
insertedRowId db
|
||||
liftIO $
|
||||
liftIO $ do
|
||||
DB.execute
|
||||
db
|
||||
"INSERT INTO rcv_files (file_id, file_status, file_queue_info, file_inline, rcv_file_inline, file_descr_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)"
|
||||
(fileId, FSNew, fileConnReq, fileInline, rcvFileInline, rfdId, currentTs, currentTs)
|
||||
pure RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = f, fileStatus = RFSNew, fileType = FTNormal, rcvFileInline, senderDisplayName = c, chunkSize, cancelled = False, grpMemberId = Nothing, cryptoArgs = Nothing}
|
||||
forM_ (storedBadge prohibited_ fileBadge) $ createFileBadgeProof db fileId BPKInvitation
|
||||
pure RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = f, fileProhibited = prohibited_, fileStatus = RFSNew, fileType = FTNormal, rcvFileInline, senderDisplayName = c, chunkSize, cancelled = False, grpMemberId = Nothing, cryptoArgs = Nothing}
|
||||
|
||||
createRcvGroupFileTransfer :: DB.Connection -> UserId -> GroupInfo -> Maybe GroupMember -> FileType -> Maybe SharedMsgId -> FileInvitation -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer
|
||||
createRcvGroupFileTransfer db userId GroupInfo {groupId, localDisplayName = gName} m_ fileType sharedMsgId_ f@FileInvitation {fileName, fileSize, fileDigest, fileConnReq, fileInline, fileDescr} rcvFileInline chunkSize = do
|
||||
setFileProhibited :: DB.Connection -> User -> Int64 -> FileProhibited -> IO ()
|
||||
setFileProhibited db User {userId} fileId FileProhibited {maxSize, badgeStatus} = do
|
||||
currentTs <- getCurrentTime
|
||||
DB.execute
|
||||
db
|
||||
"UPDATE files SET file_max_size = ?, file_badge_status = ?, updated_at = ? WHERE user_id = ? AND file_id = ?"
|
||||
(maxSize, badgeStatus, currentTs, userId, fileId)
|
||||
|
||||
prohibitedRow :: Maybe FileProhibited -> (Maybe Integer, Maybe BadgeStatus)
|
||||
prohibitedRow = \case
|
||||
Just FileProhibited {maxSize, badgeStatus} -> (Just maxSize, badgeStatus)
|
||||
Nothing -> (Nothing, Nothing)
|
||||
|
||||
-- a proof that did not verify is not stored - files.file_badge_status records that it failed
|
||||
storedBadge :: Maybe FileProhibited -> Maybe BadgeProof -> Maybe BadgeProof
|
||||
storedBadge prohibited_ badge_ = case prohibited_ of
|
||||
Just FileProhibited {badgeStatus = Just st} | st /= BSActive -> Nothing
|
||||
_ -> badge_
|
||||
|
||||
createFileBadgeProof :: DB.Connection -> Int64 -> BadgeProofKind -> BadgeProof -> IO ()
|
||||
createFileBadgeProof db fileId kind badge = do
|
||||
currentTs <- getCurrentTime
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
INSERT INTO file_badge_proofs (file_id, proof_kind, badge_proof, badge_pres_header, badge_key_idx, badge_type, badge_expiry, badge_extra, created_at, updated_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?)
|
||||
ON CONFLICT (file_id, proof_kind) DO UPDATE SET
|
||||
badge_proof = excluded.badge_proof,
|
||||
badge_pres_header = excluded.badge_pres_header,
|
||||
badge_key_idx = excluded.badge_key_idx,
|
||||
badge_type = excluded.badge_type,
|
||||
badge_expiry = excluded.badge_expiry,
|
||||
badge_extra = excluded.badge_extra,
|
||||
updated_at = excluded.updated_at
|
||||
|]
|
||||
((fileId, kind) :. badgeProofToRow badge :. (currentTs, currentTs))
|
||||
|
||||
createRcvGroupFileTransfer :: DB.Connection -> UserId -> GroupInfo -> Maybe GroupMember -> FileType -> Maybe SharedMsgId -> FileInvitation -> Maybe FileProhibited -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer
|
||||
createRcvGroupFileTransfer db userId GroupInfo {groupId, localDisplayName = gName} m_ fileType sharedMsgId_ f@FileInvitation {fileName, fileSize, fileDigest, fileConnReq, fileInline, fileDescr, fileBadge} prohibited_ rcvFileInline chunkSize = do
|
||||
currentTs <- liftIO getCurrentTime
|
||||
rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr
|
||||
let rfdId = (\RcvFileDescr {fileDescrId} -> fileDescrId) <$> rfd_
|
||||
@@ -479,15 +524,16 @@ createRcvGroupFileTransfer db userId GroupInfo {groupId, localDisplayName = gNam
|
||||
fileId <- liftIO $ do
|
||||
DB.execute
|
||||
db
|
||||
"INSERT INTO files (user_id, group_id, file_name, file_size, chunk_size, file_inline, ci_file_status, protocol, file_type, shared_msg_id, created_at, updated_at, file_digest) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)"
|
||||
((userId, groupId, fileName, fileSize, chunkSize, fileInline, CIFSRcvInvitation, fileProtocol, fileType, sharedMsgId_, currentTs, currentTs) :. Only fileDigest)
|
||||
"INSERT INTO files (user_id, group_id, file_name, file_size, chunk_size, file_inline, ci_file_status, protocol, file_type, shared_msg_id, created_at, updated_at, file_digest, file_max_size, file_badge_status) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
|
||||
((userId, groupId, fileName, fileSize, chunkSize, fileInline, CIFSRcvInvitation, fileProtocol, fileType, sharedMsgId_, currentTs, currentTs) :. Only fileDigest :. prohibitedRow prohibited_)
|
||||
insertedRowId db
|
||||
liftIO $
|
||||
liftIO $ do
|
||||
DB.execute
|
||||
db
|
||||
"INSERT INTO rcv_files (file_id, file_status, file_queue_info, file_inline, rcv_file_inline, group_member_id, file_descr_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)"
|
||||
(fileId, FSNew, fileConnReq, fileInline, rcvFileInline, grpMemberId_, rfdId, currentTs, currentTs)
|
||||
pure RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = f, fileStatus = RFSNew, fileType, rcvFileInline, senderDisplayName = senderName, chunkSize, cancelled = False, grpMemberId = grpMemberId_, cryptoArgs = Nothing}
|
||||
forM_ (storedBadge prohibited_ fileBadge) $ createFileBadgeProof db fileId BPKInvitation
|
||||
pure RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = f, fileProhibited = prohibited_, fileStatus = RFSNew, fileType, rcvFileInline, senderDisplayName = senderName, chunkSize, cancelled = False, grpMemberId = grpMemberId_, cryptoArgs = Nothing}
|
||||
|
||||
-- Roster scratch file owned by a per-source transfer: group_member_id is the delivering relay (so chunk
|
||||
-- streams from different relays are distinct files), roster_transfer_id links to the metadata record.
|
||||
@@ -506,7 +552,7 @@ createRosterRcvFile db userId GroupInfo {groupId} src@GroupMember {localDisplayN
|
||||
db
|
||||
"INSERT INTO rcv_files (file_id, file_status, file_queue_info, file_inline, rcv_file_inline, group_member_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)"
|
||||
(fileId, FSNew, fileConnReq, fileInline, rcvFileInline, grpMemberId_, currentTs, currentTs)
|
||||
pure RcvFileTransfer {fileId, xftpRcvFile = Nothing, fileInvitation = f, fileStatus = RFSNew, fileType = FTRoster, rcvFileInline, senderDisplayName = senderName, chunkSize, cancelled = False, grpMemberId = Just grpMemberId_, cryptoArgs = Nothing}
|
||||
pure RcvFileTransfer {fileId, xftpRcvFile = Nothing, fileInvitation = f, fileProhibited = Nothing, fileStatus = RFSNew, fileType = FTRoster, rcvFileInline, senderDisplayName = senderName, chunkSize, cancelled = False, grpMemberId = Just grpMemberId_, cryptoArgs = Nothing}
|
||||
|
||||
createRcvStandaloneFileTransfer :: DB.Connection -> UserId -> CryptoFile -> Int64 -> Word32 -> ExceptT StoreError IO Int64
|
||||
createRcvStandaloneFileTransfer db userId (CryptoFile filePath cfArgs_) fileSize chunkSize = do
|
||||
@@ -586,7 +632,7 @@ rcvFileDescrWithinLimits partNo descrText =
|
||||
&& T.length descrText <= maxRcvFileDescrTextLength
|
||||
|
||||
getRcvFileDescrByRcvFileId :: DB.Connection -> FileTransferId -> ExceptT StoreError IO RcvFileDescr
|
||||
getRcvFileDescrByRcvFileId db fileId = do
|
||||
getRcvFileDescrByRcvFileId db fileId =
|
||||
liftIO (getRcvFileDescrByRcvFileId_ db fileId) >>= \case
|
||||
Nothing -> throwError $ SERcvFileDescrNotFound fileId
|
||||
Just rfd -> pure rfd
|
||||
@@ -625,6 +671,19 @@ getRcvFileDescrBySndFileId_ db fileId =
|
||||
|]
|
||||
(Only fileId)
|
||||
|
||||
getFileBadgeProofs :: DB.Connection -> Int64 -> IO (Maybe BadgeProof, Maybe BadgeProof)
|
||||
getFileBadgeProofs db fileId = foldl' addProof (Nothing, Nothing) <$> DB.query db q (Only fileId)
|
||||
where
|
||||
q =
|
||||
[sql|
|
||||
SELECT proof_kind, badge_proof, badge_pres_header, badge_key_idx, badge_type, badge_expiry, badge_extra
|
||||
FROM file_badge_proofs
|
||||
WHERE file_id = ?
|
||||
|]
|
||||
addProof (inv_, descr_) (Only kind :. row) = case kind of
|
||||
BPKInvitation -> (rowToBadgeProof row, descr_)
|
||||
BPKDescription -> (inv_, rowToBadgeProof row)
|
||||
|
||||
toRcvFileDescr :: (Int64, Text, Int, BoolInt) -> RcvFileDescr
|
||||
toRcvFileDescr (fileDescrId, fileDescrText, fileDescrPartNo, BI fileDescrComplete) =
|
||||
RcvFileDescr {fileDescrId, fileDescrText, fileDescrPartNo, fileDescrComplete}
|
||||
@@ -652,7 +711,8 @@ getRcvFileTransfer_ db userId fileId = do
|
||||
SELECT r.file_status, r.file_queue_info, r.group_member_id, f.file_name,
|
||||
f.file_size, f.chunk_size, f.cancelled, cs.local_display_name, m.local_display_name,
|
||||
f.file_path, f.file_crypto_key, f.file_crypto_nonce, r.file_inline, r.rcv_file_inline,
|
||||
r.agent_rcv_file_id, r.agent_rcv_file_deleted, r.user_approved_relays, g.local_display_name, f.file_type, f.file_digest
|
||||
r.agent_rcv_file_id, r.agent_rcv_file_deleted, r.user_approved_relays, g.local_display_name, f.file_type, f.file_digest,
|
||||
f.file_max_size, f.file_badge_status
|
||||
FROM rcv_files r
|
||||
JOIN files f USING (file_id)
|
||||
LEFT JOIN contacts cs ON cs.contact_id = f.contact_id
|
||||
@@ -666,9 +726,9 @@ getRcvFileTransfer_ db userId fileId = do
|
||||
where
|
||||
rcvFileTransfer ::
|
||||
Maybe RcvFileDescr ->
|
||||
(FileStatus, Maybe ConnReqInvitation, Maybe Int64, String, Integer, Integer, Maybe BoolInt) :. (Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe InlineFileMode, Maybe InlineFileMode, Maybe AgentRcvFileId, BoolInt, BoolInt) :. (Maybe ContactName, FileType, Maybe FileDigest) ->
|
||||
(FileStatus, Maybe ConnReqInvitation, Maybe Int64, String, Integer, Integer, Maybe BoolInt) :. (Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe InlineFileMode, Maybe InlineFileMode, Maybe AgentRcvFileId, BoolInt, BoolInt) :. (Maybe ContactName, FileType, Maybe FileDigest, Maybe Integer, Maybe BadgeStatus) ->
|
||||
ExceptT StoreError IO RcvFileTransfer
|
||||
rcvFileTransfer rfd_ ((fileStatus', fileConnReq, grpMemberId, fileName, fileSize, chunkSize, cancelled_) :. (contactName_, memberName_, filePath_, fileKey, fileNonce, fileInline, rcvFileInline, agentRcvFileId, BI agentRcvFileDeleted, BI userApprovedRelays) :. (groupName_, fileType, fileDigest_)) =
|
||||
rcvFileTransfer rfd_ ((fileStatus', fileConnReq, grpMemberId, fileName, fileSize, chunkSize, cancelled_) :. (contactName_, memberName_, filePath_, fileKey, fileNonce, fileInline, rcvFileInline, agentRcvFileId, BI agentRcvFileDeleted, BI userApprovedRelays) :. (groupName_, fileType, fileDigest_, fileMaxSize_, fileBadgeStatus_)) =
|
||||
case contactName_ <|> memberName_ <|> groupName_ <|> standaloneName_ of
|
||||
Nothing -> throwError $ SERcvFileInvalid fileId
|
||||
Just name ->
|
||||
@@ -683,10 +743,11 @@ getRcvFileTransfer_ db userId fileId = do
|
||||
(Just _, Just _) -> Just "" -- filePath marks files that are accepted from contact or, in this case, set by createRcvDirectFileTransfer
|
||||
_ -> Nothing
|
||||
ft senderDisplayName fileStatus =
|
||||
let fileInvitation = FileInvitation {fileName, fileSize, fileDigest = fileDigest_, fileConnReq, fileInline, fileDescr = Nothing}
|
||||
let fileInvitation = FileInvitation {fileName, fileSize, fileDigest = fileDigest_, fileConnReq, fileInline, fileDescr = Nothing, fileBadge = Nothing}
|
||||
fileProhibited = (\maxSize -> FileProhibited {maxSize, badgeStatus = fileBadgeStatus_}) <$> fileMaxSize_
|
||||
cryptoArgs = CFArgs <$> fileKey <*> fileNonce
|
||||
xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId, agentRcvFileDeleted, userApprovedRelays}) <$> rfd_
|
||||
in RcvFileTransfer {fileId, xftpRcvFile, fileInvitation, fileStatus, fileType, rcvFileInline, senderDisplayName, chunkSize, cancelled, grpMemberId, cryptoArgs}
|
||||
in RcvFileTransfer {fileId, xftpRcvFile, fileInvitation, fileProhibited, fileStatus, fileType, rcvFileInline, senderDisplayName, chunkSize, cancelled, grpMemberId, cryptoArgs}
|
||||
filePath = case filePath_ of
|
||||
Nothing -> throwError $ SERcvFileInvalid fileId
|
||||
Just fp -> pure fp
|
||||
|
||||
@@ -164,6 +164,7 @@ import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Time (addUTCTime)
|
||||
import Data.Time.Clock (UTCTime (..), getCurrentTime)
|
||||
import Simplex.Chat.Badges (BadgeStatus)
|
||||
import Simplex.Chat.Controller (ChatListQuery (..), ChatPagination (..), PaginationByTime (..))
|
||||
import Simplex.Chat.Markdown
|
||||
import Simplex.Chat.Messages
|
||||
@@ -1093,7 +1094,7 @@ getLocalChatPreview_ db user (LocalChatPD _ noteFolderId lastItemId_ stats) = do
|
||||
|
||||
-- this function can be changed so it never fails, not only avoid failure on invalid json
|
||||
toLocalChatItem :: UTCTime -> ChatItemRow -> Either StoreError (CChatItem 'CTLocal)
|
||||
toLocalChatItem currentTs ((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sentViaProxy, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive, BI userMention, BI hasLink, msgSigned) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_, fileExpires)) =
|
||||
toLocalChatItem currentTs ((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sentViaProxy, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive, BI userMention, BI hasLink, msgSigned) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_, fileExpires) :. (fileMaxSize_, fileBadgeStatus_)) =
|
||||
chatItem $ fromRight invalid $ dbParseACIContent itemContentText
|
||||
where
|
||||
invalid = ACIContent msgDir $ CIInvalidJSON itemContentText
|
||||
@@ -1113,7 +1114,8 @@ toLocalChatItem currentTs ((itemId, itemTs, AMsgDirection msgDir, itemContentTex
|
||||
(Just fileId, Just fileName, Just fileSize, Just fileProtocol) ->
|
||||
let cfArgs = CFArgs <$> fileKey <*> fileNonce
|
||||
fileSource = (`CryptoFile` cfArgs) <$> filePath
|
||||
in Just CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol, fileExpires}
|
||||
fileProhibited = (\maxSize -> FileProhibited {maxSize, badgeStatus = fileBadgeStatus_}) <$> fileMaxSize_
|
||||
in Just CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol, fileExpires, fileProhibited}
|
||||
_ -> Nothing
|
||||
cItem :: MsgDirectionI d => SMsgDirection d -> CIDirection 'CTLocal d -> CIStatus d -> CIContent d -> Maybe (CIFile d) -> CChatItem 'CTLocal
|
||||
cItem d chatDir ciStatus content file =
|
||||
@@ -2276,7 +2278,7 @@ updateLocalChatItemsRead db User {userId} noteFolderId = do
|
||||
|]
|
||||
(CISRcvRead, currentTs, userId, noteFolderId, CISRcvNew)
|
||||
|
||||
type MaybeCIFIleRow = (Maybe Int64, Maybe String, Maybe Integer, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe ACIFileStatus, Maybe FileProtocol, Maybe UTCTime)
|
||||
type MaybeCIFIleRow = (Maybe Int64, Maybe String, Maybe Integer, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe ACIFileStatus, Maybe FileProtocol, Maybe UTCTime) :. (Maybe Integer, Maybe BadgeStatus)
|
||||
|
||||
type ChatItemModeRow = (Maybe Int, Maybe UTCTime, Maybe BoolInt, BoolInt, BoolInt, Maybe MsgVerified)
|
||||
|
||||
@@ -2304,7 +2306,7 @@ toQuote (quotedItemId, quotedSharedMsgId, quotedSentAt, quotedMsgContent, _) dir
|
||||
|
||||
-- this function can be changed so it never fails, not only avoid failure on invalid json
|
||||
toDirectChatItem :: UTCTime -> ChatItemRow :. QuoteRow -> Either StoreError (CChatItem 'CTDirect)
|
||||
toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sentViaProxy, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive, BI userMention, BI hasLink, msgSigned) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_, fileExpires)) :. quoteRow) =
|
||||
toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sentViaProxy, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive, BI userMention, BI hasLink, msgSigned) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_, fileExpires) :. (fileMaxSize_, fileBadgeStatus_)) :. quoteRow) =
|
||||
chatItem $ fromRight invalid $ dbParseACIContent itemContentText
|
||||
where
|
||||
invalid = ACIContent msgDir $ CIInvalidJSON itemContentText
|
||||
@@ -2324,7 +2326,8 @@ toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentT
|
||||
(Just fileId, Just fileName, Just fileSize, Just fileProtocol) ->
|
||||
let cfArgs = CFArgs <$> fileKey <*> fileNonce
|
||||
fileSource = (`CryptoFile` cfArgs) <$> filePath
|
||||
in Just CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol, fileExpires}
|
||||
fileProhibited = (\maxSize -> FileProhibited {maxSize, badgeStatus = fileBadgeStatus_}) <$> fileMaxSize_
|
||||
in Just CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol, fileExpires, fileProhibited}
|
||||
_ -> Nothing
|
||||
cItem :: MsgDirectionI d => SMsgDirection d -> CIDirection 'CTDirect d -> CIStatus d -> CIContent d -> Maybe (CIFile d) -> CChatItem 'CTDirect
|
||||
cItem d chatDir ciStatus content file =
|
||||
@@ -2380,6 +2383,7 @@ toGroupChatItem
|
||||
:. forwardedFromRow
|
||||
:. (timedTTL, timedDeleteAt, itemLive, BI userMention, BI hasLink, msgSigned)
|
||||
:. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_, fileExpires)
|
||||
:. (fileMaxSize_, fileBadgeStatus_)
|
||||
)
|
||||
:. (forwardedByMember, BI showGroupAsSender)
|
||||
:. memberRow_
|
||||
@@ -2414,7 +2418,8 @@ toGroupChatItem
|
||||
(Just fileId, Just fileName, Just fileSize, Just fileProtocol) ->
|
||||
let cfArgs = CFArgs <$> fileKey <*> fileNonce
|
||||
fileSource = (`CryptoFile` cfArgs) <$> filePath
|
||||
in Just CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol, fileExpires}
|
||||
fileProhibited = (\maxSize -> FileProhibited {maxSize, badgeStatus = fileBadgeStatus_}) <$> fileMaxSize_
|
||||
in Just CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol, fileExpires, fileProhibited}
|
||||
_ -> Nothing
|
||||
cItem :: MsgDirectionI d => SMsgDirection d -> CIDirection 'CTGroup d -> CIStatus d -> CIContent d -> Maybe (CIFile d) -> CChatItem 'CTGroup
|
||||
cItem d chatDir ciStatus content file =
|
||||
@@ -2705,7 +2710,7 @@ getDirectChatItem db User {userId} contactId itemId = ExceptT $ do
|
||||
i.fwd_from_group_type, i.fwd_from_group_link, i.fwd_from_public_group_id, i.fwd_from_member_id, i.fwd_from_shared_msg_id,
|
||||
i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, i.has_link, i.msg_signed,
|
||||
-- CIFile
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, f.file_expires_at,
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, f.file_expires_at, f.file_max_size, f.file_badge_status,
|
||||
-- DirectQuote
|
||||
ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent
|
||||
FROM chat_items i
|
||||
@@ -3099,7 +3104,7 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do
|
||||
i.fwd_from_group_type, i.fwd_from_group_link, i.fwd_from_public_group_id, i.fwd_from_member_id, i.fwd_from_shared_msg_id,
|
||||
i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, i.has_link, i.msg_signed,
|
||||
-- CIFile
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, f.file_expires_at,
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, f.file_expires_at, f.file_max_size, f.file_badge_status,
|
||||
-- CIMeta forwardedByMember, showGroupAsSender
|
||||
i.forwarded_by_group_member_id, i.show_group_as_sender,
|
||||
-- GroupMember
|
||||
@@ -3212,7 +3217,7 @@ getLocalChatItem db User {userId} folderId itemId = ExceptT $ do
|
||||
i.fwd_from_group_type, i.fwd_from_group_link, i.fwd_from_public_group_id, i.fwd_from_member_id, i.fwd_from_shared_msg_id,
|
||||
i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, i.has_link, i.msg_signed,
|
||||
-- CIFile
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, f.file_expires_at
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, f.file_expires_at, f.file_max_size, f.file_badge_status
|
||||
FROM chat_items i
|
||||
LEFT JOIN files f ON f.chat_item_id = i.chat_item_id
|
||||
WHERE i.user_id = ? AND i.note_folder_id = ? AND i.chat_item_id = ?
|
||||
|
||||
@@ -49,6 +49,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20260723_contact_request_rejectio
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260813_auto_accept_group_invitations
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260822_forward_link
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260828_file_expiry
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260904_file_badges
|
||||
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
|
||||
|
||||
schemaMigrations :: [(String, Text, Maybe Text)]
|
||||
@@ -97,7 +98,8 @@ schemaMigrations =
|
||||
("20260723_contact_request_rejection", m20260723_contact_request_rejection, Just down_m20260723_contact_request_rejection),
|
||||
("20260813_auto_accept_group_invitations", m20260813_auto_accept_group_invitations, Just down_m20260813_auto_accept_group_invitations),
|
||||
("20260822_forward_link", m20260822_forward_link, Just down_m20260822_forward_link),
|
||||
("20260828_file_expiry", m20260828_file_expiry, Just down_m20260828_file_expiry)
|
||||
("20260828_file_expiry", m20260828_file_expiry, Just down_m20260828_file_expiry),
|
||||
("20260904_file_badges", m20260904_file_badges, Just down_m20260904_file_badges)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Store.Postgres.Migrations.M20260904_file_badges where
|
||||
|
||||
import Data.Text (Text)
|
||||
import Text.RawString.QQ (r)
|
||||
|
||||
m20260904_file_badges :: Text
|
||||
m20260904_file_badges =
|
||||
[r|
|
||||
ALTER TABLE files ADD COLUMN file_max_size BIGINT;
|
||||
ALTER TABLE files ADD COLUMN file_badge_status TEXT;
|
||||
|
||||
CREATE TABLE file_badge_proofs(
|
||||
badge_proof_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
file_id BIGINT NOT NULL REFERENCES files ON DELETE CASCADE,
|
||||
proof_kind TEXT NOT NULL,
|
||||
badge_proof BYTEA NOT NULL,
|
||||
badge_pres_header BYTEA NOT NULL,
|
||||
badge_key_idx BIGINT NOT NULL,
|
||||
badge_type TEXT NOT NULL,
|
||||
badge_expiry TIMESTAMPTZ NOT NULL,
|
||||
badge_extra TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX idx_file_badge_proofs_file_id_kind ON file_badge_proofs(file_id, proof_kind);
|
||||
|]
|
||||
|
||||
down_m20260904_file_badges :: Text
|
||||
down_m20260904_file_badges =
|
||||
[r|
|
||||
DROP INDEX idx_file_badge_proofs_file_id_kind;
|
||||
DROP TABLE file_badge_proofs;
|
||||
|
||||
ALTER TABLE files DROP COLUMN file_badge_status;
|
||||
ALTER TABLE files DROP COLUMN file_max_size;
|
||||
|]
|
||||
@@ -744,6 +744,33 @@ ALTER TABLE test_chat_schema.extra_xftp_file_descriptions ALTER COLUMN extra_fil
|
||||
|
||||
|
||||
|
||||
CREATE TABLE test_chat_schema.file_badge_proofs (
|
||||
badge_proof_id bigint NOT NULL,
|
||||
file_id bigint NOT NULL,
|
||||
proof_kind text NOT NULL,
|
||||
badge_proof bytea NOT NULL,
|
||||
badge_pres_header bytea NOT NULL,
|
||||
badge_key_idx bigint NOT NULL,
|
||||
badge_type text NOT NULL,
|
||||
badge_expiry timestamp with time zone NOT NULL,
|
||||
badge_extra text NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
ALTER TABLE test_chat_schema.file_badge_proofs ALTER COLUMN badge_proof_id ADD GENERATED ALWAYS AS IDENTITY (
|
||||
SEQUENCE NAME test_chat_schema.file_badge_proofs_badge_proof_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE test_chat_schema.files (
|
||||
file_id bigint NOT NULL,
|
||||
contact_id bigint,
|
||||
@@ -771,7 +798,9 @@ CREATE TABLE test_chat_schema.files (
|
||||
file_type text DEFAULT 'normal'::text NOT NULL,
|
||||
roster_transfer_id bigint,
|
||||
file_digest bytea,
|
||||
file_expires_at timestamp with time zone
|
||||
file_expires_at timestamp with time zone,
|
||||
file_max_size bigint,
|
||||
file_badge_status text
|
||||
);
|
||||
|
||||
|
||||
@@ -1691,6 +1720,11 @@ ALTER TABLE ONLY test_chat_schema.extra_xftp_file_descriptions
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY test_chat_schema.file_badge_proofs
|
||||
ADD CONSTRAINT file_badge_proofs_pkey PRIMARY KEY (badge_proof_id);
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY test_chat_schema.files
|
||||
ADD CONSTRAINT files_pkey PRIMARY KEY (file_id);
|
||||
|
||||
@@ -2336,6 +2370,10 @@ CREATE INDEX idx_extra_xftp_file_descriptions_user_id ON test_chat_schema.extra_
|
||||
|
||||
|
||||
|
||||
CREATE UNIQUE INDEX idx_file_badge_proofs_file_id_kind ON test_chat_schema.file_badge_proofs USING btree (file_id, proof_kind);
|
||||
|
||||
|
||||
|
||||
CREATE INDEX idx_files_chat_item_id ON test_chat_schema.files USING btree (chat_item_id);
|
||||
|
||||
|
||||
@@ -2980,6 +3018,11 @@ ALTER TABLE ONLY test_chat_schema.extra_xftp_file_descriptions
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY test_chat_schema.file_badge_proofs
|
||||
ADD CONSTRAINT file_badge_proofs_file_id_fkey FOREIGN KEY (file_id) REFERENCES test_chat_schema.files(file_id) ON DELETE CASCADE;
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY test_chat_schema.files
|
||||
ADD CONSTRAINT files_contact_id_fkey FOREIGN KEY (contact_id) REFERENCES test_chat_schema.contacts(contact_id) ON DELETE CASCADE;
|
||||
|
||||
|
||||
@@ -172,6 +172,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20260723_contact_request_rejection
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260813_auto_accept_group_invitations
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260822_forward_link
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260828_file_expiry
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260904_file_badges
|
||||
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
|
||||
|
||||
schemaMigrations :: [(String, Query, Maybe Query)]
|
||||
@@ -343,7 +344,8 @@ schemaMigrations =
|
||||
("20260723_contact_request_rejection", m20260723_contact_request_rejection, Just down_m20260723_contact_request_rejection),
|
||||
("20260813_auto_accept_group_invitations", m20260813_auto_accept_group_invitations, Just down_m20260813_auto_accept_group_invitations),
|
||||
("20260822_forward_link", m20260822_forward_link, Just down_m20260822_forward_link),
|
||||
("20260828_file_expiry", m20260828_file_expiry, Just down_m20260828_file_expiry)
|
||||
("20260828_file_expiry", m20260828_file_expiry, Just down_m20260828_file_expiry),
|
||||
("20260904_file_badges", m20260904_file_badges, Just down_m20260904_file_badges)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Store.SQLite.Migrations.M20260904_file_badges where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20260904_file_badges :: Query
|
||||
m20260904_file_badges =
|
||||
[sql|
|
||||
ALTER TABLE files ADD COLUMN file_max_size INTEGER;
|
||||
ALTER TABLE files ADD COLUMN file_badge_status TEXT;
|
||||
|
||||
CREATE TABLE file_badge_proofs(
|
||||
badge_proof_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
file_id INTEGER NOT NULL REFERENCES files ON DELETE CASCADE,
|
||||
proof_kind TEXT NOT NULL,
|
||||
badge_proof BLOB NOT NULL,
|
||||
badge_pres_header BLOB NOT NULL,
|
||||
badge_key_idx INTEGER NOT NULL,
|
||||
badge_type TEXT NOT NULL,
|
||||
badge_expiry TEXT NOT NULL,
|
||||
badge_extra TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
) STRICT;
|
||||
|
||||
CREATE UNIQUE INDEX idx_file_badge_proofs_file_id_kind ON file_badge_proofs(file_id, proof_kind);
|
||||
|]
|
||||
|
||||
down_m20260904_file_badges :: Query
|
||||
down_m20260904_file_badges =
|
||||
[sql|
|
||||
DROP INDEX idx_file_badge_proofs_file_id_kind;
|
||||
DROP TABLE file_badge_proofs;
|
||||
|
||||
ALTER TABLE files DROP COLUMN file_badge_status;
|
||||
ALTER TABLE files DROP COLUMN file_max_size;
|
||||
|]
|
||||
@@ -1357,7 +1357,7 @@ Query:
|
||||
i.fwd_from_group_type, i.fwd_from_group_link, i.fwd_from_public_group_id, i.fwd_from_member_id, i.fwd_from_shared_msg_id,
|
||||
i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, i.has_link, i.msg_signed,
|
||||
-- CIFile
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, f.file_expires_at
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, f.file_expires_at, f.file_max_size, f.file_badge_status
|
||||
FROM chat_items i
|
||||
LEFT JOIN files f ON f.chat_item_id = i.chat_item_id
|
||||
WHERE i.user_id = ? AND i.note_folder_id = ? AND i.chat_item_id = ?
|
||||
@@ -1375,7 +1375,7 @@ Query:
|
||||
i.fwd_from_group_type, i.fwd_from_group_link, i.fwd_from_public_group_id, i.fwd_from_member_id, i.fwd_from_shared_msg_id,
|
||||
i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, i.has_link, i.msg_signed,
|
||||
-- CIFile
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, f.file_expires_at,
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, f.file_expires_at, f.file_max_size, f.file_badge_status,
|
||||
-- CIMeta forwardedByMember, showGroupAsSender
|
||||
i.forwarded_by_group_member_id, i.show_group_as_sender,
|
||||
-- GroupMember
|
||||
@@ -1432,7 +1432,7 @@ Query:
|
||||
i.fwd_from_group_type, i.fwd_from_group_link, i.fwd_from_public_group_id, i.fwd_from_member_id, i.fwd_from_shared_msg_id,
|
||||
i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, i.has_link, i.msg_signed,
|
||||
-- CIFile
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, f.file_expires_at,
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, f.file_expires_at, f.file_max_size, f.file_badge_status,
|
||||
-- DirectQuote
|
||||
ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent
|
||||
FROM chat_items i
|
||||
@@ -1721,7 +1721,8 @@ Query:
|
||||
SELECT r.file_status, r.file_queue_info, r.group_member_id, f.file_name,
|
||||
f.file_size, f.chunk_size, f.cancelled, cs.local_display_name, m.local_display_name,
|
||||
f.file_path, f.file_crypto_key, f.file_crypto_nonce, r.file_inline, r.rcv_file_inline,
|
||||
r.agent_rcv_file_id, r.agent_rcv_file_deleted, r.user_approved_relays, g.local_display_name, f.file_type, f.file_digest
|
||||
r.agent_rcv_file_id, r.agent_rcv_file_deleted, r.user_approved_relays, g.local_display_name, f.file_type, f.file_digest,
|
||||
f.file_max_size, f.file_badge_status
|
||||
FROM rcv_files r
|
||||
JOIN files f USING (file_id)
|
||||
LEFT JOIN contacts cs ON cs.contact_id = f.contact_id
|
||||
@@ -3972,6 +3973,14 @@ SEARCH pgm USING INDEX idx_pending_group_messages_group_member_id (group_member_
|
||||
SEARCH m USING INTEGER PRIMARY KEY (rowid=?)
|
||||
USE TEMP B-TREE FOR ORDER BY
|
||||
|
||||
Query:
|
||||
SELECT proof_kind, badge_proof, badge_pres_header, badge_key_idx, badge_type, badge_expiry, badge_extra
|
||||
FROM file_badge_proofs
|
||||
WHERE file_id = ?
|
||||
|
||||
Plan:
|
||||
SEARCH file_badge_proofs USING INDEX idx_file_badge_proofs_file_id_kind (file_id=?)
|
||||
|
||||
Query:
|
||||
SELECT r.contact_id, g.group_id, r.group_member_id
|
||||
FROM received_probes r
|
||||
@@ -4763,6 +4772,7 @@ Plan:
|
||||
SEARCH files USING INDEX idx_files_user_id (user_id=?)
|
||||
LIST SUBQUERY 1
|
||||
SEARCH chat_items USING COVERING INDEX idx_chat_items_note_folder_has_link_created_at (user_id=? AND note_folder_id=?)
|
||||
SEARCH file_badge_proofs USING COVERING INDEX idx_file_badge_proofs_file_id_kind (file_id=?)
|
||||
SEARCH extra_xftp_file_descriptions USING COVERING INDEX idx_extra_xftp_file_descriptions_file_id (file_id=?)
|
||||
SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?)
|
||||
SEARCH snd_files USING COVERING INDEX idx_snd_files_file_id (file_id=?)
|
||||
@@ -4893,6 +4903,20 @@ Query:
|
||||
|
||||
Plan:
|
||||
|
||||
Query:
|
||||
INSERT INTO file_badge_proofs (file_id, proof_kind, badge_proof, badge_pres_header, badge_key_idx, badge_type, badge_expiry, badge_extra, created_at, updated_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?)
|
||||
ON CONFLICT (file_id, proof_kind) DO UPDATE SET
|
||||
badge_proof = excluded.badge_proof,
|
||||
badge_pres_header = excluded.badge_pres_header,
|
||||
badge_key_idx = excluded.badge_key_idx,
|
||||
badge_type = excluded.badge_type,
|
||||
badge_expiry = excluded.badge_expiry,
|
||||
badge_extra = excluded.badge_extra,
|
||||
updated_at = excluded.updated_at
|
||||
|
||||
Plan:
|
||||
|
||||
Query:
|
||||
INSERT INTO files
|
||||
( user_id, note_folder_id,
|
||||
@@ -6704,6 +6728,7 @@ SEARCH users USING INTEGER PRIMARY KEY (rowid=?)
|
||||
Query: DELETE FROM files WHERE roster_transfer_id = ?
|
||||
Plan:
|
||||
SEARCH files USING COVERING INDEX idx_files_roster_transfer_id (roster_transfer_id=?)
|
||||
SEARCH file_badge_proofs USING COVERING INDEX idx_file_badge_proofs_file_id_kind (file_id=?)
|
||||
SEARCH extra_xftp_file_descriptions USING COVERING INDEX idx_extra_xftp_file_descriptions_file_id (file_id=?)
|
||||
SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?)
|
||||
SEARCH snd_files USING COVERING INDEX idx_snd_files_file_id (file_id=?)
|
||||
@@ -6712,6 +6737,7 @@ SEARCH files USING COVERING INDEX idx_files_redirect_file_id (redirect_file_id=?
|
||||
Query: DELETE FROM files WHERE user_id = ? AND contact_id = ?
|
||||
Plan:
|
||||
SEARCH files USING INDEX idx_files_contact_id (contact_id=?)
|
||||
SEARCH file_badge_proofs USING COVERING INDEX idx_file_badge_proofs_file_id_kind (file_id=?)
|
||||
SEARCH extra_xftp_file_descriptions USING COVERING INDEX idx_extra_xftp_file_descriptions_file_id (file_id=?)
|
||||
SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?)
|
||||
SEARCH snd_files USING COVERING INDEX idx_snd_files_file_id (file_id=?)
|
||||
@@ -6720,6 +6746,7 @@ SEARCH files USING COVERING INDEX idx_files_redirect_file_id (redirect_file_id=?
|
||||
Query: DELETE FROM files WHERE user_id = ? AND group_id = ? AND file_type = ?
|
||||
Plan:
|
||||
SEARCH files USING INDEX idx_files_group_id (group_id=?)
|
||||
SEARCH file_badge_proofs USING COVERING INDEX idx_file_badge_proofs_file_id_kind (file_id=?)
|
||||
SEARCH extra_xftp_file_descriptions USING COVERING INDEX idx_extra_xftp_file_descriptions_file_id (file_id=?)
|
||||
SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?)
|
||||
SEARCH snd_files USING COVERING INDEX idx_snd_files_file_id (file_id=?)
|
||||
@@ -7023,13 +7050,13 @@ Plan:
|
||||
Query: INSERT INTO files (contact_id, group_id, user_id, file_name, file_path, file_crypto_key, file_crypto_nonce, file_size, chunk_size, redirect_file_id, agent_snd_file_id, ci_file_status, protocol, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
Plan:
|
||||
|
||||
Query: INSERT INTO files (user_id, contact_id, file_name, file_size, chunk_size, file_inline, ci_file_status, protocol, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)
|
||||
Query: INSERT INTO files (user_id, contact_id, file_name, file_size, chunk_size, file_inline, ci_file_status, protocol, file_max_size, file_badge_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
Plan:
|
||||
|
||||
Query: INSERT INTO files (user_id, file_name, file_path, file_size, chunk_size, ci_file_status, protocol, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)
|
||||
Plan:
|
||||
|
||||
Query: INSERT INTO files (user_id, group_id, file_name, file_size, chunk_size, file_inline, ci_file_status, protocol, file_type, shared_msg_id, created_at, updated_at, file_digest) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
Query: INSERT INTO files (user_id, group_id, file_name, file_size, chunk_size, file_inline, ci_file_status, protocol, file_type, shared_msg_id, created_at, updated_at, file_digest, file_max_size, file_badge_status) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
Plan:
|
||||
|
||||
Query: INSERT INTO files (user_id, group_id, file_name, file_size, chunk_size, file_inline, ci_file_status, protocol, file_type, shared_msg_id, roster_transfer_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
|
||||
@@ -302,7 +302,9 @@ CREATE TABLE files(
|
||||
file_type TEXT NOT NULL DEFAULT 'normal',
|
||||
roster_transfer_id INTEGER,
|
||||
file_digest BLOB,
|
||||
file_expires_at TEXT
|
||||
file_expires_at TEXT,
|
||||
file_max_size INTEGER,
|
||||
file_badge_status TEXT
|
||||
) STRICT;
|
||||
CREATE TABLE snd_files(
|
||||
file_id INTEGER NOT NULL REFERENCES files ON DELETE CASCADE,
|
||||
@@ -852,6 +854,19 @@ CREATE TABLE rcv_roster_transfers(
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||
) STRICT;
|
||||
CREATE TABLE file_badge_proofs(
|
||||
badge_proof_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
file_id INTEGER NOT NULL REFERENCES files ON DELETE CASCADE,
|
||||
proof_kind TEXT NOT NULL,
|
||||
badge_proof BLOB NOT NULL,
|
||||
badge_pres_header BLOB NOT NULL,
|
||||
badge_key_idx INTEGER NOT NULL,
|
||||
badge_type TEXT NOT NULL,
|
||||
badge_expiry TEXT NOT NULL,
|
||||
badge_extra TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
) STRICT;
|
||||
CREATE INDEX contact_profiles_index ON contact_profiles(
|
||||
display_name,
|
||||
full_name
|
||||
@@ -1386,6 +1401,10 @@ CREATE INDEX idx_files_roster_transfer_id ON files(roster_transfer_id);
|
||||
CREATE INDEX idx_chat_items_item_signed_by_group_member_id ON chat_items(
|
||||
item_signed_by_group_member_id
|
||||
);
|
||||
CREATE UNIQUE INDEX idx_file_badge_proofs_file_id_kind ON file_badge_proofs(
|
||||
file_id,
|
||||
proof_kind
|
||||
);
|
||||
CREATE TRIGGER on_group_members_insert_update_summary
|
||||
AFTER INSERT ON group_members
|
||||
FOR EACH ROW
|
||||
|
||||
@@ -1558,7 +1558,8 @@ data FileInvitation = FileInvitation
|
||||
fileDigest :: Maybe FileDigest,
|
||||
fileConnReq :: Maybe ConnReqInvitation,
|
||||
fileInline :: Maybe InlineFileMode,
|
||||
fileDescr :: Maybe FileDescr
|
||||
fileDescr :: Maybe FileDescr,
|
||||
fileBadge :: Maybe BadgeProof
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
@@ -1573,7 +1574,8 @@ xftpFileInvitation fileName fileSize fileDescr =
|
||||
fileDigest = Nothing,
|
||||
fileConnReq = Nothing,
|
||||
fileInline = Nothing,
|
||||
fileDescr = Just fileDescr
|
||||
fileDescr = Just fileDescr,
|
||||
fileBadge = Nothing
|
||||
}
|
||||
|
||||
data InlineFileMode
|
||||
@@ -1627,10 +1629,14 @@ instance ToJSON FileType where
|
||||
toJSON = J.String . textEncode
|
||||
toEncoding = JE.text . textEncode
|
||||
|
||||
data FileProhibited = FileProhibited {maxSize :: Integer, badgeStatus :: Maybe BadgeStatus}
|
||||
deriving (Eq, Show)
|
||||
|
||||
data RcvFileTransfer = RcvFileTransfer
|
||||
{ fileId :: FileTransferId,
|
||||
xftpRcvFile :: Maybe XFTPRcvFile,
|
||||
fileInvitation :: FileInvitation,
|
||||
fileProhibited :: Maybe FileProhibited,
|
||||
fileStatus :: RcvFileStatus,
|
||||
fileType :: FileType,
|
||||
rcvFileInline :: Maybe InlineFileMode,
|
||||
@@ -2379,6 +2385,8 @@ $(JQ.deriveJSON defaultJSON ''GroupMemberRef)
|
||||
|
||||
$(JQ.deriveJSON defaultJSON ''FileDescr)
|
||||
|
||||
$(JQ.deriveJSON defaultJSON ''FileProhibited)
|
||||
|
||||
$(JQ.deriveJSON defaultJSON ''FileInvitation)
|
||||
|
||||
$(JQ.deriveJSON defaultJSON ''SndFileTransfer)
|
||||
|
||||
@@ -2453,11 +2453,25 @@ viewReceivedFileInvitation :: StyledString -> CIFile d -> CurrentTime -> TimeZon
|
||||
viewReceivedFileInvitation from file ts tz meta = receivedWithTime_ ts tz from [] meta (receivedFileInvitation_ file) False
|
||||
|
||||
receivedFileInvitation_ :: CIFile d -> [StyledString]
|
||||
receivedFileInvitation_ CIFile {fileId, fileName, fileSize, fileStatus} =
|
||||
receivedFileInvitation_ CIFile {fileId, fileName, fileSize, fileStatus, fileProhibited} =
|
||||
["sends file " <> ttyFilePath fileName <> " (" <> humanReadableSize fileSize <> " / " <> sShow fileSize <> " bytes)"]
|
||||
<> case fileStatus of
|
||||
CIFSRcvAccepted -> []
|
||||
_ -> ["use " <> highlight ("/fr " <> show fileId <> " [<dir>/ | <path>]") <> " to receive it"]
|
||||
<> case fileProhibited of
|
||||
Just fp -> [prohibitedFileReason fp]
|
||||
Nothing -> case fileStatus of
|
||||
CIFSRcvAccepted -> []
|
||||
_ -> ["use " <> highlight ("/fr " <> show fileId <> " [<dir>/ | <path>]") <> " to receive it"]
|
||||
|
||||
prohibitedFileReason :: FileProhibited -> StyledString
|
||||
prohibitedFileReason FileProhibited {maxSize, badgeStatus} =
|
||||
"file is above the limit of " <> sShow maxSize <> " bytes: " <> reason
|
||||
where
|
||||
reason = case badgeStatus of
|
||||
Nothing -> "sender has no badge"
|
||||
Just BSActive -> "above the limit of the sender badge"
|
||||
Just BSExpired -> "sender badge expired"
|
||||
Just BSExpiredOld -> "sender badge expired"
|
||||
Just BSFailed -> "sender badge did not verify"
|
||||
Just BSUnknownKey -> "sender badge key is not known"
|
||||
|
||||
humanReadableSize :: Integer -> StyledString
|
||||
humanReadableSize size
|
||||
|
||||
Reference in New Issue
Block a user