core: api to forward messages (#3968)

* wip

* wip

* test

* mute

* tests

* simplify (only bool flag)

* re-encrypt file

* tests

* more tests (wip)

* fix relative paths, refactor

* more tests

* more locks

* fix, tests

* more tests

* rework (revert from bool to ids)

* update schema

* more tests

* add to info

* ForwardedMsg container

* Revert "ForwardedMsg container"

This reverts commit bb57f12151.

* parser

* more tests

* rework api

* more locks

* test

* move

* remove from

* view

* prohibit editing

* item info view

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
This commit is contained in:
Evgeny Poberezkin 2024-04-09 13:02:59 +01:00 committed by GitHub
parent f8e6a78a3b
commit a5db36469d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 1061 additions and 211 deletions

View file

@ -140,6 +140,7 @@ library
Simplex.Chat.Migrations.M20240228_pq Simplex.Chat.Migrations.M20240228_pq
Simplex.Chat.Migrations.M20240313_drop_agent_ack_cmd_id Simplex.Chat.Migrations.M20240313_drop_agent_ack_cmd_id
Simplex.Chat.Migrations.M20240324_custom_data Simplex.Chat.Migrations.M20240324_custom_data
Simplex.Chat.Migrations.M20240402_item_forwarded
Simplex.Chat.Mobile Simplex.Chat.Mobile
Simplex.Chat.Mobile.File Simplex.Chat.Mobile.File
Simplex.Chat.Mobile.Shared Simplex.Chat.Mobile.Shared
@ -575,6 +576,7 @@ test-suite simplex-chat-test
ChatTests.ChatList ChatTests.ChatList
ChatTests.Direct ChatTests.Direct
ChatTests.Files ChatTests.Files
ChatTests.Forward
ChatTests.Groups ChatTests.Groups
ChatTests.Local ChatTests.Local
ChatTests.Profiles ChatTests.Profiles

View file

@ -82,7 +82,7 @@ import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.Util import Simplex.Chat.Types.Util
import Simplex.Chat.Util (encryptFile, shuffle) import Simplex.Chat.Util (encryptFile, liftIOEither, shuffle)
import Simplex.FileTransfer.Client.Main (maxFileSize, maxFileSizeHard) import Simplex.FileTransfer.Client.Main (maxFileSize, maxFileSizeHard)
import Simplex.FileTransfer.Client.Presets (defaultXFTPServers) import Simplex.FileTransfer.Client.Presets (defaultXFTPServers)
import Simplex.FileTransfer.Description (FileDescriptionURI (..), ValidFileDescription) import Simplex.FileTransfer.Description (FileDescriptionURI (..), ValidFileDescription)
@ -705,105 +705,28 @@ processChatCommand' vr = \case
[] -> pure Nothing [] -> pure Nothing
memStatuses -> pure $ Just $ map (uncurry MemberDeliveryStatus) memStatuses memStatuses -> pure $ Just $ map (uncurry MemberDeliveryStatus) memStatuses
_ -> pure Nothing _ -> pure Nothing
pure $ CRChatItemInfo user aci ChatItemInfo {itemVersions, memberDeliveryStatuses} forwardedFromChatItem <- getForwardedFromItem user ci
APISendMessage (ChatRef cType chatId) live itemTTL (ComposedMessage file_ quotedItemId_ mc) -> withUser $ \user -> case cType of pure $ CRChatItemInfo user aci ChatItemInfo {itemVersions, memberDeliveryStatuses, forwardedFromChatItem}
CTDirect -> withContactLock "sendMessage" chatId $ do where
ct@Contact {contactId, contactUsed} <- withStore $ \db -> getContact db vr user chatId getForwardedFromItem :: User -> ChatItem c d -> CM (Maybe AChatItem)
assertDirectAllowed user MDSnd ct XMsgNew_ getForwardedFromItem user ChatItem {meta = CIMeta {itemForwarded}} = case itemForwarded of
unless contactUsed $ withStore' $ \db -> updateContactUsed db user ct Just (CIFFContact _ _ (Just ctId) (Just fwdItemId)) ->
if isVoice mc && not (featureAllowed SCFVoice forUser ct) Just <$> withStore (\db -> getAChatItem db vr user (ChatRef CTDirect ctId) fwdItemId)
then pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (chatFeatureNameText CFVoice)) Just (CIFFGroup _ _ (Just gId) (Just fwdItemId)) ->
else do Just <$> withStore (\db -> getAChatItem db vr user (ChatRef CTGroup gId) fwdItemId)
(fInv_, ciFile_) <- L.unzip <$> setupSndFileTransfer ct _ -> pure Nothing
timed_ <- sndContactCITimed live ct itemTTL APISendMessage (ChatRef cType chatId) live itemTTL cm -> withUser $ \user -> case cType of
(msgContainer, quotedItem_) <- prepareMsg fInv_ timed_ CTDirect ->
(msg, _) <- sendDirectContactMessage user ct (XMsgNew msgContainer) withContactLock "sendMessage" chatId $
ci <- saveSndChatItem' user (CDDirectSnd ct) msg (CISndMsgContent mc) ciFile_ quotedItem_ timed_ live sendContactContentMessage user chatId live itemTTL cm Nothing
forM_ (timed_ >>= timedDeleteAt') $ CTGroup ->
startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId' ci) withGroupLock "sendMessage" chatId $
pure $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) sendGroupContentMessage user chatId live itemTTL cm Nothing
where
setupSndFileTransfer :: Contact -> CM (Maybe (FileInvitation, CIFile 'MDSnd))
setupSndFileTransfer ct = forM file_ $ \file -> do
fileSize <- checkSndFile file
xftpSndFileTransfer user file fileSize 1 $ CGContact ct
prepareMsg :: Maybe FileInvitation -> Maybe CITimed -> CM (MsgContainer, Maybe (CIQuote 'CTDirect))
prepareMsg fInv_ timed_ = case quotedItemId_ of
Nothing -> pure (MCSimple (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Nothing)
Just quotedItemId -> do
CChatItem _ qci@ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, formattedText, file} <-
withStore $ \db -> getDirectChatItem db user chatId quotedItemId
(origQmc, qd, sent) <- quoteData qci
let msgRef = MsgRef {msgId = itemSharedMsgId, sentAt = itemTs, sent, memberId = Nothing}
qmc = quoteContent mc origQmc file
quotedItem = CIQuote {chatDir = qd, itemId = Just quotedItemId, sharedMsgId = itemSharedMsgId, sentAt = itemTs, content = qmc, formattedText}
pure (MCQuote QuotedMsg {msgRef, content = qmc} (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Just quotedItem)
where
quoteData :: ChatItem c d -> CM (MsgContent, CIQDirection 'CTDirect, Bool)
quoteData ChatItem {meta = CIMeta {itemDeleted = Just _}} = throwChatError CEInvalidQuote
quoteData ChatItem {content = CISndMsgContent qmc} = pure (qmc, CIQDirectSnd, True)
quoteData ChatItem {content = CIRcvMsgContent qmc} = pure (qmc, CIQDirectRcv, False)
quoteData _ = throwChatError CEInvalidQuote
CTGroup -> withGroupLock "sendMessage" chatId $ do
g@(Group gInfo _) <- withStore $ \db -> getGroup db vr user chatId
assertUserGroupRole gInfo GRAuthor
send g
where
send g@(Group gInfo@GroupInfo {groupId, membership} ms) =
case prohibitedGroupContent gInfo membership mc file_ of
Just f -> notAllowedError f
Nothing -> do
(fInv_, ciFile_) <- L.unzip <$> setupSndFileTransfer g (length $ filter memberCurrent ms)
timed_ <- sndGroupCITimed live gInfo itemTTL
(msgContainer, quotedItem_) <- prepareGroupMsg user gInfo mc quotedItemId_ fInv_ timed_ live
(msg, sentToMembers) <- sendGroupMessage user gInfo ms (XMsgNew msgContainer)
ci <- saveSndChatItem' user (CDGroupSnd gInfo) msg (CISndMsgContent mc) ciFile_ quotedItem_ timed_ live
withStore' $ \db ->
forM_ sentToMembers $ \GroupMember {groupMemberId} ->
createGroupSndStatus db (chatItemId' ci) groupMemberId CISSndNew
forM_ (timed_ >>= timedDeleteAt') $
startProximateTimedItemThread user (ChatRef CTGroup groupId, chatItemId' ci)
pure $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci)
notAllowedError f = pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (groupFeatureNameText f))
setupSndFileTransfer :: Group -> Int -> CM (Maybe (FileInvitation, CIFile 'MDSnd))
setupSndFileTransfer g n = forM file_ $ \file -> do
fileSize <- checkSndFile file
xftpSndFileTransfer user file fileSize n $ CGGroup g
CTLocal -> pure $ chatCmdError (Just user) "not supported" CTLocal -> pure $ chatCmdError (Just user) "not supported"
CTContactRequest -> pure $ chatCmdError (Just user) "not supported" CTContactRequest -> pure $ chatCmdError (Just user) "not supported"
CTContactConnection -> pure $ chatCmdError (Just user) "not supported" CTContactConnection -> pure $ chatCmdError (Just user) "not supported"
where APICreateChatItem folderId cm -> withUser $ \user ->
xftpSndFileTransfer :: User -> CryptoFile -> Integer -> Int -> ContactOrGroup -> CM (FileInvitation, CIFile 'MDSnd) createNoteFolderContentItem user folderId cm Nothing
xftpSndFileTransfer user file fileSize n contactOrGroup = do
(fInv, ciFile, ft) <- xftpSndFileTransfer_ user file fileSize n $ Just contactOrGroup
case contactOrGroup of
CGContact Contact {activeConn} -> forM_ activeConn $ \conn ->
withStore' $ \db -> createSndFTDescrXFTP db user Nothing conn ft dummyFileDescr
CGGroup (Group _ ms) -> forM_ ms $ \m -> saveMemberFD m `catchChatError` (toView . CRChatError (Just user))
where
-- we are not sending files to pending members, same as with inline files
saveMemberFD m@GroupMember {activeConn = Just conn@Connection {connStatus}} =
when ((connStatus == ConnReady || connStatus == ConnSndReady) && not (connDisabled conn)) $
withStore' $
\db -> createSndFTDescrXFTP db user (Just m) conn ft dummyFileDescr
saveMemberFD _ = pure ()
pure (fInv, ciFile)
APICreateChatItem folderId (ComposedMessage file_ quotedItemId_ mc) -> withUser $ \user -> do
forM_ quotedItemId_ $ \_ -> throwError $ ChatError $ CECommandError "not supported"
nf <- withStore $ \db -> getNoteFolder db user folderId
createdAt <- liftIO getCurrentTime
let content = CISndMsgContent mc
let cd = CDLocalSnd nf
ciId <- createLocalChatItem user cd content createdAt
ciFile_ <- forM file_ $ \cf@CryptoFile {filePath, cryptoArgs} -> do
fsFilePath <- lift $ toFSFilePath filePath
fileSize <- liftIO $ CF.getFileContentsSize $ CryptoFile fsFilePath cryptoArgs
chunkSize <- asks $ fileChunkSize . config
withStore' $ \db -> do
fileId <- createLocalFile CIFSSndStored db user nf ciId createdAt cf fileSize chunkSize
pure CIFile {fileId, fileName = takeFileName filePath, fileSize, fileSource = Just cf, fileStatus = CIFSSndStored, fileProtocol = FPLocal}
let ci = mkChatItem cd ciId content ciFile_ Nothing Nothing Nothing False createdAt Nothing createdAt
pure . CRNewChatItem user $ AChatItem SCTLocal SMDSnd (LocalChat nf) ci
APIUpdateChatItem (ChatRef cType chatId) itemId live mc -> withUser $ \user -> case cType of APIUpdateChatItem (ChatRef cType chatId) itemId live mc -> withUser $ \user -> case cType of
CTDirect -> withContactLock "updateChatItem" chatId $ do CTDirect -> withContactLock "updateChatItem" chatId $ do
ct@Contact {contactId} <- withStore $ \db -> getContact db vr user chatId ct@Contact {contactId} <- withStore $ \db -> getContact db vr user chatId
@ -948,6 +871,91 @@ processChatCommand' vr = \case
throwChatError (CECommandError $ "reaction already " <> if add then "added" else "removed") throwChatError (CECommandError $ "reaction already " <> if add then "added" else "removed")
when (add && length rs >= maxMsgReactions) $ when (add && length rs >= maxMsgReactions) $
throwChatError (CECommandError "too many reactions") throwChatError (CECommandError "too many reactions")
APIForwardChatItem (ChatRef toCType toChatId) (ChatRef fromCType fromChatId) itemId -> withUser $ \user -> case toCType of
CTDirect -> do
(cm, ciff) <- prepareForward user
withContactLock "forwardChatItem, to contact" toChatId $
sendContactContentMessage user toChatId False Nothing cm ciff
CTGroup -> do
(cm, ciff) <- prepareForward user
withGroupLock "forwardChatItem, to group" toChatId $
sendGroupContentMessage user toChatId False Nothing cm ciff
CTLocal -> do
(cm, ciff) <- prepareForward user
createNoteFolderContentItem user toChatId cm ciff
CTContactRequest -> pure $ chatCmdError (Just user) "not supported"
CTContactConnection -> pure $ chatCmdError (Just user) "not supported"
where
prepareForward :: User -> CM (ComposedMessage, Maybe CIForwardedFrom)
prepareForward user = case fromCType of
CTDirect -> withContactLock "forwardChatItem, from contact" fromChatId $ do
(ct, CChatItem _ ci) <- withStore $ \db -> do
ct <- getContact db vr user fromChatId
cci <- getDirectChatItem db user fromChatId itemId
pure (ct, cci)
(mc, mDir) <- forwardMC ci
file <- forwardCryptoFile ci `catchChatError` (\e -> toView (CRChatError (Just user) e) $> Nothing)
let ciff = forwardCIFF ci $ Just (CIFFContact (forwardName ct) mDir (Just fromChatId) (Just itemId))
pure (ComposedMessage file Nothing mc, ciff)
where
forwardName :: Contact -> ContactName
forwardName Contact {profile = LocalProfile {displayName, localAlias}}
| localAlias /= "" = localAlias
| otherwise = displayName
CTGroup -> withGroupLock "forwardChatItem, from group" fromChatId $ do
(gInfo, CChatItem _ ci) <- withStore $ \db -> do
gInfo <- getGroupInfo db vr user fromChatId
cci <- getGroupChatItem db user fromChatId itemId
pure (gInfo, cci)
(mc, mDir) <- forwardMC ci
file <- forwardCryptoFile ci `catchChatError` (\e -> toView (CRChatError (Just user) e) $> Nothing)
let ciff = forwardCIFF ci $ Just (CIFFGroup (forwardName gInfo) mDir (Just fromChatId) (Just itemId))
pure (ComposedMessage file Nothing mc, ciff)
where
forwardName :: GroupInfo -> ContactName
forwardName GroupInfo {groupProfile = GroupProfile {displayName}} = displayName
CTLocal -> do
(CChatItem _ ci) <- withStore $ \db -> getLocalChatItem db user fromChatId itemId
(mc, _) <- forwardMC ci
file <- forwardCryptoFile ci `catchChatError` (\e -> toView (CRChatError (Just user) e) $> Nothing)
let ciff = forwardCIFF ci Nothing
pure (ComposedMessage file Nothing mc, ciff)
CTContactRequest -> throwChatError $ CECommandError "not supported"
CTContactConnection -> throwChatError $ CECommandError "not supported"
where
forwardMC :: ChatItem c d -> CM (MsgContent, MsgDirection)
forwardMC ChatItem {meta = CIMeta {itemDeleted = Just _}} = throwChatError CEInvalidForward
forwardMC ChatItem {content = CISndMsgContent fmc} = pure (fmc, MDSnd)
forwardMC ChatItem {content = CIRcvMsgContent fmc} = pure (fmc, MDRcv)
forwardMC _ = throwChatError CEInvalidForward
forwardCIFF :: ChatItem c d -> Maybe CIForwardedFrom -> Maybe CIForwardedFrom
forwardCIFF ChatItem {meta = CIMeta {itemForwarded = Just ciff}} _ = Just ciff
forwardCIFF _ ciff = ciff
forwardCryptoFile :: ChatItem c d -> CM (Maybe CryptoFile)
forwardCryptoFile ChatItem {file = Just CIFile {fileName, fileStatus, fileSource = Just fromCF@CryptoFile {filePath}}}
| ciFileLoaded fileStatus =
chatReadVar filesFolder >>= \case
Nothing ->
ifM (doesFileExist filePath) (pure $ Just fromCF) (pure Nothing)
Just filesFolder -> do
let fsFromPath = filesFolder </> filePath
ifM
(doesFileExist fsFromPath)
( do
fsNewPath <- liftIO $ filesFolder `uniqueCombine` fileName
liftIO $ B.writeFile fsNewPath "" -- create empty file
encrypt <- chatReadVar encryptLocalFiles
cfArgs <- if encrypt then Just <$> (atomically . CF.randomArgs =<< asks random) else pure Nothing
let toCF = CryptoFile fsNewPath cfArgs
-- to keep forwarded file in case original is deleted
liftIOEither $ runExceptT $ withExceptT (ChatError . CEInternalError . show) $ do
lb <- CF.readFile (fromCF {filePath = fsFromPath} :: CryptoFile)
CF.writeFile toCF lb
pure $ Just (toCF {filePath = takeFileName fsNewPath} :: CryptoFile)
)
(pure Nothing)
| otherwise = pure Nothing
forwardCryptoFile _ = pure Nothing
APIUserRead userId -> withUserId userId $ \user -> withStore' (`setUserChatsRead` user) >> ok user APIUserRead userId -> withUserId userId $ \user -> withStore' (`setUserChatsRead` user) >> ok user
UserRead -> withUser $ \User {userId} -> processChatCommand $ APIUserRead userId UserRead -> withUser $ \User {userId} -> processChatCommand $ APIUserRead userId
APIChatRead (ChatRef cType chatId) fromToIds -> withUser $ \_ -> case cType of APIChatRead (ChatRef cType chatId) fromToIds -> withUser $ \_ -> case cType of
@ -1554,6 +1562,21 @@ processChatCommand' vr = \case
RejectContact cName -> withUser $ \User {userId} -> do RejectContact cName -> withUser $ \User {userId} -> do
connReqId <- withStore $ \db -> getContactRequestIdByName db userId cName connReqId <- withStore $ \db -> getContactRequestIdByName db userId cName
processChatCommand $ APIRejectContact connReqId processChatCommand $ APIRejectContact connReqId
ForwardMessage toChatName fromContactName forwardedMsg -> withUser $ \user -> do
contactId <- withStore $ \db -> getContactIdByName db user fromContactName
forwardedItemId <- withStore $ \db -> getDirectChatItemIdByText' db user contactId forwardedMsg
toChatRef <- getChatRef user toChatName
processChatCommand $ APIForwardChatItem toChatRef (ChatRef CTDirect contactId) forwardedItemId
ForwardGroupMessage toChatName fromGroupName fromMemberName_ forwardedMsg -> withUser $ \user -> do
groupId <- withStore $ \db -> getGroupIdByName db user fromGroupName
forwardedItemId <- withStore $ \db -> getGroupChatItemIdByText db user groupId fromMemberName_ forwardedMsg
toChatRef <- getChatRef user toChatName
processChatCommand $ APIForwardChatItem toChatRef (ChatRef CTGroup groupId) forwardedItemId
ForwardLocalMessage toChatName forwardedMsg -> withUser $ \user -> do
folderId <- withStore (`getUserNoteFolderId` user)
forwardedItemId <- withStore $ \db -> getLocalChatItemIdByText' db user folderId forwardedMsg
toChatRef <- getChatRef user toChatName
processChatCommand $ APIForwardChatItem toChatRef (ChatRef CTLocal folderId) forwardedItemId
SendMessage (ChatName cType name) msg -> withUser $ \user -> do SendMessage (ChatName cType name) msg -> withUser $ \user -> do
let mc = MCText msg let mc = MCText msg
case cType of case cType of
@ -1638,7 +1661,7 @@ processChatCommand' vr = \case
combineResults _ _ (Left e) = Left e combineResults _ _ (Left e) = Left e
createCI :: DB.Connection -> User -> UTCTime -> (Contact, SndMessage) -> IO () createCI :: DB.Connection -> User -> UTCTime -> (Contact, SndMessage) -> IO ()
createCI db user createdAt (ct, sndMsg) = createCI db user createdAt (ct, sndMsg) =
void $ createNewSndChatItem db user (CDDirectSnd ct) sndMsg (CISndMsgContent mc) Nothing Nothing False createdAt void $ createNewSndChatItem db user (CDDirectSnd ct) sndMsg (CISndMsgContent mc) Nothing Nothing Nothing False createdAt
SendMessageQuote cName (AMsgDirection msgDir) quotedMsg msg -> withUser $ \user@User {userId} -> do SendMessageQuote cName (AMsgDirection msgDir) quotedMsg msg -> withUser $ \user@User {userId} -> do
contactId <- withStore $ \db -> getContactIdByName db user cName contactId <- withStore $ \db -> getContactIdByName db user cName
quotedItemId <- withStore $ \db -> getDirectChatItemIdByText db userId contactId msgDir quotedMsg quotedItemId <- withStore $ \db -> getDirectChatItemIdByText db userId contactId msgDir quotedMsg
@ -2060,7 +2083,7 @@ processChatCommand' vr = \case
p {groupPreferences = Just . setGroupPreference f enabled $ groupPreferences p} p {groupPreferences = Just . setGroupPreference f enabled $ groupPreferences p}
SetGroupFeatureRole (AGFR f) gName enabled role -> SetGroupFeatureRole (AGFR f) gName enabled role ->
updateGroupProfileByName gName $ \p -> updateGroupProfileByName gName $ \p ->
p {groupPreferences = Just . setGroupPreferenceRole f enabled role $ groupPreferences p} p {groupPreferences = Just . setGroupPreferenceRole f enabled role $ groupPreferences p}
SetUserTimedMessages onOff -> withUser $ \user@User {profile} -> do SetUserTimedMessages onOff -> withUser $ \user@User {profile} -> do
let allowed = if onOff then FAYes else FANo let allowed = if onOff then FAYes else FANo
pref = TimedMessagesPreference allowed Nothing pref = TimedMessagesPreference allowed Nothing
@ -2587,6 +2610,104 @@ processChatCommand' vr = \case
let aciContent = ACIContent SMDRcv $ CIRcvGroupInvitation ciGroupInv {status = newStatus} memRole let aciContent = ACIContent SMDRcv $ CIRcvGroupInvitation ciGroupInv {status = newStatus} memRole
updateDirectChatItemView user ct itemId aciContent False Nothing updateDirectChatItemView user ct itemId aciContent False Nothing
_ -> pure () -- prohibited _ -> pure () -- prohibited
sendContactContentMessage :: User -> ContactId -> Bool -> Maybe Int -> ComposedMessage -> Maybe CIForwardedFrom -> CM ChatResponse
sendContactContentMessage user contactId live itemTTL (ComposedMessage file_ quotedItemId_ mc) itemForwarded = do
ct@Contact {contactUsed} <- withStore $ \db -> getContact db vr user contactId
assertDirectAllowed user MDSnd ct XMsgNew_
unless contactUsed $ withStore' $ \db -> updateContactUsed db user ct
if isVoice mc && not (featureAllowed SCFVoice forUser ct)
then pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (chatFeatureNameText CFVoice))
else do
(fInv_, ciFile_) <- L.unzip <$> setupSndFileTransfer ct
timed_ <- sndContactCITimed live ct itemTTL
(msgContainer, quotedItem_) <- prepareMsg fInv_ timed_
(msg, _) <- sendDirectContactMessage user ct (XMsgNew msgContainer)
ci <- saveSndChatItem' user (CDDirectSnd ct) msg (CISndMsgContent mc) ciFile_ quotedItem_ itemForwarded timed_ live
forM_ (timed_ >>= timedDeleteAt') $
startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId' ci)
pure $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci)
where
setupSndFileTransfer :: Contact -> CM (Maybe (FileInvitation, CIFile 'MDSnd))
setupSndFileTransfer ct = forM file_ $ \file -> do
fileSize <- checkSndFile file
xftpSndFileTransfer user file fileSize 1 $ CGContact ct
prepareMsg :: Maybe FileInvitation -> Maybe CITimed -> CM (MsgContainer, Maybe (CIQuote 'CTDirect))
prepareMsg fInv_ timed_ = case (quotedItemId_, itemForwarded) of
(Nothing, Nothing) -> pure (MCSimple (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Nothing)
(Nothing, Just _) -> pure (MCForward (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Nothing)
(Just quotedItemId, Nothing) -> do
CChatItem _ qci@ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, formattedText, file} <-
withStore $ \db -> getDirectChatItem db user contactId quotedItemId
(origQmc, qd, sent) <- quoteData qci
let msgRef = MsgRef {msgId = itemSharedMsgId, sentAt = itemTs, sent, memberId = Nothing}
qmc = quoteContent mc origQmc file
quotedItem = CIQuote {chatDir = qd, itemId = Just quotedItemId, sharedMsgId = itemSharedMsgId, sentAt = itemTs, content = qmc, formattedText}
pure (MCQuote QuotedMsg {msgRef, content = qmc} (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Just quotedItem)
(Just _, Just _) -> throwChatError CEInvalidQuote
where
quoteData :: ChatItem c d -> CM (MsgContent, CIQDirection 'CTDirect, Bool)
quoteData ChatItem {meta = CIMeta {itemDeleted = Just _}} = throwChatError CEInvalidQuote
quoteData ChatItem {content = CISndMsgContent qmc} = pure (qmc, CIQDirectSnd, True)
quoteData ChatItem {content = CIRcvMsgContent qmc} = pure (qmc, CIQDirectRcv, False)
quoteData _ = throwChatError CEInvalidQuote
sendGroupContentMessage :: User -> GroupId -> Bool -> Maybe Int -> ComposedMessage -> Maybe CIForwardedFrom -> CM ChatResponse
sendGroupContentMessage user groupId live itemTTL (ComposedMessage file_ quotedItemId_ mc) itemForwarded = do
g@(Group gInfo _) <- withStore $ \db -> getGroup db vr user groupId
assertUserGroupRole gInfo GRAuthor
send g
where
send g@(Group gInfo@GroupInfo {membership} ms) =
case prohibitedGroupContent gInfo membership mc file_ of
Just f -> notAllowedError f
Nothing -> do
(fInv_, ciFile_) <- L.unzip <$> setupSndFileTransfer g (length $ filter memberCurrent ms)
timed_ <- sndGroupCITimed live gInfo itemTTL
(msgContainer, quotedItem_) <- prepareGroupMsg user gInfo mc quotedItemId_ itemForwarded fInv_ timed_ live
(msg, sentToMembers) <- sendGroupMessage user gInfo ms (XMsgNew msgContainer)
ci <- saveSndChatItem' user (CDGroupSnd gInfo) msg (CISndMsgContent mc) ciFile_ quotedItem_ itemForwarded timed_ live
withStore' $ \db ->
forM_ sentToMembers $ \GroupMember {groupMemberId} ->
createGroupSndStatus db (chatItemId' ci) groupMemberId CISSndNew
forM_ (timed_ >>= timedDeleteAt') $
startProximateTimedItemThread user (ChatRef CTGroup groupId, chatItemId' ci)
pure $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci)
notAllowedError f = pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (groupFeatureNameText f))
setupSndFileTransfer :: Group -> Int -> CM (Maybe (FileInvitation, CIFile 'MDSnd))
setupSndFileTransfer g n = forM file_ $ \file -> do
fileSize <- checkSndFile file
xftpSndFileTransfer user file fileSize n $ CGGroup g
xftpSndFileTransfer :: User -> CryptoFile -> Integer -> Int -> ContactOrGroup -> CM (FileInvitation, CIFile 'MDSnd)
xftpSndFileTransfer user file fileSize n contactOrGroup = do
(fInv, ciFile, ft) <- xftpSndFileTransfer_ user file fileSize n $ Just contactOrGroup
case contactOrGroup of
CGContact Contact {activeConn} -> forM_ activeConn $ \conn ->
withStore' $ \db -> createSndFTDescrXFTP db user Nothing conn ft dummyFileDescr
CGGroup (Group _ ms) -> forM_ ms $ \m -> saveMemberFD m `catchChatError` (toView . CRChatError (Just user))
where
-- we are not sending files to pending members, same as with inline files
saveMemberFD m@GroupMember {activeConn = Just conn@Connection {connStatus}} =
when ((connStatus == ConnReady || connStatus == ConnSndReady) && not (connDisabled conn)) $
withStore' $
\db -> createSndFTDescrXFTP db user (Just m) conn ft dummyFileDescr
saveMemberFD _ = pure ()
pure (fInv, ciFile)
createNoteFolderContentItem :: User -> NoteFolderId -> ComposedMessage -> Maybe CIForwardedFrom -> CM ChatResponse
createNoteFolderContentItem user folderId (ComposedMessage file_ quotedItemId_ mc) itemForwarded = do
forM_ quotedItemId_ $ \_ -> throwError $ ChatError $ CECommandError "not supported"
nf <- withStore $ \db -> getNoteFolder db user folderId
createdAt <- liftIO getCurrentTime
let content = CISndMsgContent mc
let cd = CDLocalSnd nf
ciId <- createLocalChatItem user cd content itemForwarded createdAt
ciFile_ <- forM file_ $ \cf@CryptoFile {filePath, cryptoArgs} -> do
fsFilePath <- lift $ toFSFilePath filePath
fileSize <- liftIO $ CF.getFileContentsSize $ CryptoFile fsFilePath cryptoArgs
chunkSize <- asks $ fileChunkSize . config
withStore' $ \db -> do
fileId <- createLocalFile CIFSSndStored db user nf ciId createdAt cf fileSize chunkSize
pure CIFile {fileId, fileName = takeFileName filePath, fileSize, fileSource = Just cf, fileStatus = CIFSSndStored, fileProtocol = FPLocal}
let ci = mkChatItem cd ciId content ciFile_ Nothing Nothing itemForwarded Nothing False createdAt Nothing createdAt
pure . CRNewChatItem user $ AChatItem SCTLocal SMDSnd (LocalChat nf) ci
toggleNtf :: User -> GroupMember -> Bool -> CM () toggleNtf :: User -> GroupMember -> Bool -> CM ()
toggleNtf user m ntfOn = toggleNtf user m ntfOn =
@ -2601,10 +2722,11 @@ data ChangedProfileContact = ChangedProfileContact
conn :: Connection conn :: Connection
} }
prepareGroupMsg :: User -> GroupInfo -> MsgContent -> Maybe ChatItemId -> Maybe FileInvitation -> Maybe CITimed -> Bool -> CM (MsgContainer, Maybe (CIQuote 'CTGroup)) prepareGroupMsg :: User -> GroupInfo -> MsgContent -> Maybe ChatItemId -> Maybe CIForwardedFrom -> Maybe FileInvitation -> Maybe CITimed -> Bool -> CM (MsgContainer, Maybe (CIQuote 'CTGroup))
prepareGroupMsg user GroupInfo {groupId, membership} mc quotedItemId_ fInv_ timed_ live = case quotedItemId_ of prepareGroupMsg user GroupInfo {groupId, membership} mc quotedItemId_ itemForwarded fInv_ timed_ live = case (quotedItemId_, itemForwarded) of
Nothing -> pure (MCSimple (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Nothing) (Nothing, Nothing) -> pure (MCSimple (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Nothing)
Just quotedItemId -> do (Nothing, Just _) -> pure (MCForward (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Nothing)
(Just quotedItemId, Nothing) -> do
CChatItem _ qci@ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, formattedText, file} <- CChatItem _ qci@ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, formattedText, file} <-
withStore $ \db -> getGroupChatItem db user groupId quotedItemId withStore $ \db -> getGroupChatItem db user groupId quotedItemId
(origQmc, qd, sent, GroupMember {memberId}) <- quoteData qci membership (origQmc, qd, sent, GroupMember {memberId}) <- quoteData qci membership
@ -2612,6 +2734,7 @@ prepareGroupMsg user GroupInfo {groupId, membership} mc quotedItemId_ fInv_ time
qmc = quoteContent mc origQmc file qmc = quoteContent mc origQmc file
quotedItem = CIQuote {chatDir = qd, itemId = Just quotedItemId, sharedMsgId = itemSharedMsgId, sentAt = itemTs, content = qmc, formattedText} quotedItem = CIQuote {chatDir = qd, itemId = Just quotedItemId, sharedMsgId = itemSharedMsgId, sentAt = itemTs, content = qmc, formattedText}
pure (MCQuote QuotedMsg {msgRef, content = qmc} (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Just quotedItem) pure (MCQuote QuotedMsg {msgRef, content = qmc} (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Just quotedItem)
(Just _, Just _) -> throwChatError CEInvalidQuote
where where
quoteData :: ChatItem c d -> GroupMember -> CM (MsgContent, CIQDirection 'CTGroup, Bool, GroupMember) quoteData :: ChatItem c d -> GroupMember -> CM (MsgContent, CIQDirection 'CTGroup, Bool, GroupMember)
quoteData ChatItem {meta = CIMeta {itemDeleted = Just _}} _ = throwChatError CEInvalidQuote quoteData ChatItem {meta = CIMeta {itemDeleted = Just _}} _ = throwChatError CEInvalidQuote
@ -4049,7 +4172,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
let CIMeta {itemTs, itemSharedMsgId, itemTimed} = meta let CIMeta {itemTs, itemSharedMsgId, itemTimed} = meta
quotedItemId_ = quoteItemId =<< quotedItem quotedItemId_ = quoteItemId =<< quotedItem
fInv_ = fst <$> fInvDescr_ fInv_ = fst <$> fInvDescr_
(msgContainer, _) <- prepareGroupMsg user gInfo mc quotedItemId_ fInv_ itemTimed False (msgContainer, _) <- prepareGroupMsg user gInfo mc quotedItemId_ Nothing fInv_ itemTimed False
let senderVRange = memberChatVRange' sender let senderVRange = memberChatVRange' sender
xMsgNewChatMsg = ChatMessage {chatVRange = senderVRange, msgId = itemSharedMsgId, chatMsgEvent = XMsgNew msgContainer} xMsgNewChatMsg = ChatMessage {chatVRange = senderVRange, msgId = itemSharedMsgId, chatMsgEvent = XMsgNew msgContainer}
fileDescrEvents <- case (snd <$> fInvDescr_, itemSharedMsgId) of fileDescrEvents <- case (snd <$> fInvDescr_, itemSharedMsgId) of
@ -4669,18 +4792,19 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
updateRcvChatItem = do updateRcvChatItem = do
cci <- withStore $ \db -> getDirectChatItemBySharedMsgId db user contactId sharedMsgId cci <- withStore $ \db -> getDirectChatItemBySharedMsgId db user contactId sharedMsgId
case cci of case cci of
CChatItem SMDRcv ci@ChatItem {meta = CIMeta {itemLive}, content = CIRcvMsgContent oldMC} -> do CChatItem SMDRcv ci@ChatItem {meta = CIMeta {itemForwarded, itemLive}, content = CIRcvMsgContent oldMC}
let changed = mc /= oldMC | isNothing itemForwarded -> do
if changed || fromMaybe False itemLive let changed = mc /= oldMC
then do if changed || fromMaybe False itemLive
ci' <- withStore' $ \db -> do then do
when changed $ ci' <- withStore' $ \db -> do
addInitialAndNewCIVersions db (chatItemId' ci) (chatItemTs' ci, oldMC) (brokerTs, mc) when changed $
reactions <- getDirectCIReactions db ct sharedMsgId addInitialAndNewCIVersions db (chatItemId' ci) (chatItemTs' ci, oldMC) (brokerTs, mc)
updateDirectChatItem' db user contactId ci {reactions} content live $ Just msgId reactions <- getDirectCIReactions db ct sharedMsgId
toView $ CRChatItemUpdated user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci') updateDirectChatItem' db user contactId ci {reactions} content live $ Just msgId
startUpdatedTimedItemThread user (ChatRef CTDirect contactId) ci ci' toView $ CRChatItemUpdated user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci')
else toView $ CRChatItemNotChanged user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci) startUpdatedTimedItemThread user (ChatRef CTDirect contactId) ci ci'
else toView $ CRChatItemNotChanged user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci)
_ -> messageError "x.msg.update: contact attempted invalid message update" _ -> messageError "x.msg.update: contact attempted invalid message update"
messageDelete :: Contact -> SharedMsgId -> RcvMessage -> MsgMeta -> CM () messageDelete :: Contact -> SharedMsgId -> RcvMessage -> MsgMeta -> CM ()
@ -6441,17 +6565,17 @@ saveGroupFwdRcvMsg user groupId forwardingMember refAuthorMember@GroupMember {me
_ -> throwError e _ -> throwError e
saveSndChatItem :: ChatTypeI c => User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> CM (ChatItem c 'MDSnd) saveSndChatItem :: ChatTypeI c => User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> CM (ChatItem c 'MDSnd)
saveSndChatItem user cd msg content = saveSndChatItem' user cd msg content Nothing Nothing Nothing False saveSndChatItem user cd msg content = saveSndChatItem' user cd msg content Nothing Nothing Nothing Nothing False
saveSndChatItem' :: ChatTypeI c => User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> Maybe (CIFile 'MDSnd) -> Maybe (CIQuote c) -> Maybe CITimed -> Bool -> CM (ChatItem c 'MDSnd) saveSndChatItem' :: ChatTypeI c => User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> Maybe (CIFile 'MDSnd) -> Maybe (CIQuote c) -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> CM (ChatItem c 'MDSnd)
saveSndChatItem' user cd msg@SndMessage {sharedMsgId} content ciFile quotedItem itemTimed live = do saveSndChatItem' user cd msg@SndMessage {sharedMsgId} content ciFile quotedItem itemForwarded itemTimed live = do
createdAt <- liftIO getCurrentTime createdAt <- liftIO getCurrentTime
ciId <- withStore' $ \db -> do ciId <- withStore' $ \db -> do
when (ciRequiresAttention content) $ updateChatTs db user cd createdAt when (ciRequiresAttention content) $ updateChatTs db user cd createdAt
ciId <- createNewSndChatItem db user cd msg content quotedItem itemTimed live createdAt ciId <- createNewSndChatItem db user cd msg content quotedItem itemForwarded itemTimed live createdAt
forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt
pure ciId pure ciId
pure $ mkChatItem cd ciId content ciFile quotedItem (Just sharedMsgId) itemTimed live createdAt Nothing createdAt pure $ mkChatItem cd ciId content ciFile quotedItem (Just sharedMsgId) itemForwarded itemTimed live createdAt Nothing createdAt
saveRcvChatItem :: (ChatTypeI c, ChatTypeQuotable c) => User -> ChatDirection c 'MDRcv -> RcvMessage -> UTCTime -> CIContent 'MDRcv -> CM (ChatItem c 'MDRcv) saveRcvChatItem :: (ChatTypeI c, ChatTypeQuotable c) => User -> ChatDirection c 'MDRcv -> RcvMessage -> UTCTime -> CIContent 'MDRcv -> CM (ChatItem c 'MDRcv)
saveRcvChatItem user cd msg@RcvMessage {sharedMsgId_} brokerTs content = saveRcvChatItem user cd msg@RcvMessage {sharedMsgId_} brokerTs content =
@ -6460,18 +6584,18 @@ saveRcvChatItem user cd msg@RcvMessage {sharedMsgId_} brokerTs content =
saveRcvChatItem' :: (ChatTypeI c, ChatTypeQuotable c) => User -> ChatDirection c 'MDRcv -> RcvMessage -> Maybe SharedMsgId -> UTCTime -> CIContent 'MDRcv -> Maybe (CIFile 'MDRcv) -> Maybe CITimed -> Bool -> CM (ChatItem c 'MDRcv) saveRcvChatItem' :: (ChatTypeI c, ChatTypeQuotable c) => User -> ChatDirection c 'MDRcv -> RcvMessage -> Maybe SharedMsgId -> UTCTime -> CIContent 'MDRcv -> Maybe (CIFile 'MDRcv) -> Maybe CITimed -> Bool -> CM (ChatItem c 'MDRcv)
saveRcvChatItem' user cd msg@RcvMessage {forwardedByMember} sharedMsgId_ brokerTs content ciFile itemTimed live = do saveRcvChatItem' user cd msg@RcvMessage {forwardedByMember} sharedMsgId_ brokerTs content ciFile itemTimed live = do
createdAt <- liftIO getCurrentTime createdAt <- liftIO getCurrentTime
(ciId, quotedItem) <- withStore' $ \db -> do (ciId, quotedItem, itemForwarded) <- withStore' $ \db -> do
when (ciRequiresAttention content) $ updateChatTs db user cd createdAt when (ciRequiresAttention content) $ updateChatTs db user cd createdAt
(ciId, quotedItem) <- createNewRcvChatItem db user cd msg sharedMsgId_ content itemTimed live brokerTs createdAt r@(ciId, _, _) <- createNewRcvChatItem db user cd msg sharedMsgId_ content itemTimed live brokerTs createdAt
forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt
pure (ciId, quotedItem) pure r
pure $ mkChatItem cd ciId content ciFile quotedItem sharedMsgId_ itemTimed live brokerTs forwardedByMember createdAt pure $ mkChatItem cd ciId content ciFile quotedItem sharedMsgId_ itemForwarded itemTimed live brokerTs forwardedByMember createdAt
mkChatItem :: (ChatTypeI c, MsgDirectionI d) => ChatDirection c d -> ChatItemId -> CIContent d -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> Maybe CITimed -> Bool -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> ChatItem c d mkChatItem :: (ChatTypeI c, MsgDirectionI d) => ChatDirection c d -> ChatItemId -> CIContent d -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> ChatItem c d
mkChatItem cd ciId content file quotedItem sharedMsgId itemTimed live itemTs forwardedByMember currentTs = mkChatItem cd ciId content file quotedItem sharedMsgId itemForwarded itemTimed live itemTs forwardedByMember currentTs =
let itemText = ciContentToText content let itemText = ciContentToText content
itemStatus = ciCreateStatus content itemStatus = ciCreateStatus content
meta = mkCIMeta ciId content itemText itemStatus sharedMsgId Nothing False itemTimed (justTrue live) currentTs itemTs forwardedByMember currentTs currentTs meta = mkCIMeta ciId content itemText itemStatus sharedMsgId itemForwarded Nothing False itemTimed (justTrue live) currentTs itemTs forwardedByMember currentTs currentTs
in ChatItem {chatDir = toCIDirection cd, meta, content, formattedText = parseMaybeMarkdownList itemText, quotedItem, reactions = [], file} in ChatItem {chatDir = toCIDirection cd, meta, content, formattedText = parseMaybeMarkdownList itemText, quotedItem, reactions = [], file}
deleteDirectCI :: MsgDirectionI d => User -> Contact -> ChatItem 'CTDirect d -> Bool -> Bool -> CM ChatResponse deleteDirectCI :: MsgDirectionI d => User -> Contact -> ChatItem 'CTDirect d -> Bool -> Bool -> CM ChatResponse
@ -6732,17 +6856,17 @@ createInternalItemsForChats user itemTs_ dirsCIContents = do
createACIs :: DB.Connection -> UTCTime -> UTCTime -> ChatDirection c d -> [CIContent d] -> [IO AChatItem] createACIs :: DB.Connection -> UTCTime -> UTCTime -> ChatDirection c d -> [CIContent d] -> [IO AChatItem]
createACIs db itemTs createdAt cd = map $ \content -> do createACIs db itemTs createdAt cd = map $ \content -> do
ciId <- createNewChatItemNoMsg db user cd content itemTs createdAt ciId <- createNewChatItemNoMsg db user cd content itemTs createdAt
let ci = mkChatItem cd ciId content Nothing Nothing Nothing Nothing False itemTs Nothing createdAt let ci = mkChatItem cd ciId content Nothing Nothing Nothing Nothing Nothing False itemTs Nothing createdAt
pure $ AChatItem (chatTypeI @c) (msgDirection @d) (toChatInfo cd) ci pure $ AChatItem (chatTypeI @c) (msgDirection @d) (toChatInfo cd) ci
createLocalChatItem :: MsgDirectionI d => User -> ChatDirection 'CTLocal d -> CIContent d -> UTCTime -> CM ChatItemId createLocalChatItem :: MsgDirectionI d => User -> ChatDirection 'CTLocal d -> CIContent d -> Maybe CIForwardedFrom -> UTCTime -> CM ChatItemId
createLocalChatItem user cd content createdAt = do createLocalChatItem user cd content itemForwarded createdAt = do
gVar <- asks random gVar <- asks random
withStore $ \db -> do withStore $ \db -> do
liftIO $ updateChatTs db user cd createdAt liftIO $ updateChatTs db user cd createdAt
createWithRandomId gVar $ \sharedMsgId -> createWithRandomId gVar $ \sharedMsgId ->
let smi_ = Just (SharedMsgId sharedMsgId) let smi_ = Just (SharedMsgId sharedMsgId)
in createNewChatItem_ db user cd Nothing smi_ content (Nothing, Nothing, Nothing, Nothing, Nothing) Nothing False createdAt Nothing createdAt in createNewChatItem_ db user cd Nothing smi_ content (Nothing, Nothing, Nothing, Nothing, Nothing) itemForwarded Nothing False createdAt Nothing createdAt
withUser' :: (User -> CM ChatResponse) -> CM ChatResponse withUser' :: (User -> CM ChatResponse) -> CM ChatResponse
withUser' action = withUser' action =
@ -6869,6 +6993,7 @@ chatCommandP =
"/_delete item " *> (APIDeleteChatItem <$> chatRefP <* A.space <*> A.decimal <* A.space <*> ciDeleteMode), "/_delete item " *> (APIDeleteChatItem <$> chatRefP <* A.space <*> A.decimal <* A.space <*> ciDeleteMode),
"/_delete member item #" *> (APIDeleteMemberChatItem <$> A.decimal <* A.space <*> A.decimal <* A.space <*> A.decimal), "/_delete member item #" *> (APIDeleteMemberChatItem <$> A.decimal <* A.space <*> A.decimal <* A.space <*> A.decimal),
"/_reaction " *> (APIChatItemReaction <$> chatRefP <* A.space <*> A.decimal <* A.space <*> onOffP <* A.space <*> jsonP), "/_reaction " *> (APIChatItemReaction <$> chatRefP <* A.space <*> A.decimal <* A.space <*> onOffP <* A.space <*> jsonP),
"/_forward " *> (APIForwardChatItem <$> chatRefP <* A.space <*> chatRefP <* A.space <*> A.decimal),
"/_read user " *> (APIUserRead <$> A.decimal), "/_read user " *> (APIUserRead <$> A.decimal),
"/read user" $> UserRead, "/read user" $> UserRead,
"/_read chat " *> (APIChatRead <$> chatRefP <*> optional (A.space *> ((,) <$> ("from=" *> A.decimal) <* A.space <*> ("to=" *> A.decimal)))), "/_read chat " *> (APIChatRead <$> chatRefP <*> optional (A.space *> ((,) <$> ("from=" *> A.decimal) <* A.space <*> ("to=" *> A.decimal)))),
@ -7010,6 +7135,10 @@ chatCommandP =
"/_set incognito :" *> (APISetConnectionIncognito <$> A.decimal <* A.space <*> onOffP), "/_set incognito :" *> (APISetConnectionIncognito <$> A.decimal <* A.space <*> onOffP),
("/connect" <|> "/c") *> (Connect <$> incognitoP <* A.space <*> ((Just <$> strP) <|> A.takeTill isSpace $> Nothing)), ("/connect" <|> "/c") *> (Connect <$> incognitoP <* A.space <*> ((Just <$> strP) <|> A.takeTill isSpace $> Nothing)),
("/connect" <|> "/c") *> (AddContact <$> incognitoP), ("/connect" <|> "/c") *> (AddContact <$> incognitoP),
ForwardMessage <$> chatNameP <* " <- @" <*> displayName <* A.space <*> msgTextP,
ForwardGroupMessage <$> chatNameP <* " <- #" <*> displayName <* A.space <* A.char '@' <*> (Just <$> displayName) <* A.space <*> msgTextP,
ForwardGroupMessage <$> chatNameP <* " <- #" <*> displayName <*> pure Nothing <* A.space <*> msgTextP,
ForwardLocalMessage <$> chatNameP <* " <- * " <*> msgTextP,
SendMessage <$> chatNameP <* A.space <*> msgTextP, SendMessage <$> chatNameP <* A.space <*> msgTextP,
"/* " *> (SendMessage (ChatName CTLocal "") <$> msgTextP), "/* " *> (SendMessage (ChatName CTLocal "") <$> msgTextP),
"@#" *> (SendMemberContactMessage <$> displayName <* A.space <* char_ '@' <*> displayName <* A.space <*> msgTextP), "@#" *> (SendMemberContactMessage <$> displayName <* A.space <* char_ '@' <*> displayName <* A.space <*> msgTextP),

View file

@ -292,6 +292,7 @@ data ChatCommand
| APIDeleteChatItem ChatRef ChatItemId CIDeleteMode | APIDeleteChatItem ChatRef ChatItemId CIDeleteMode
| APIDeleteMemberChatItem GroupId GroupMemberId ChatItemId | APIDeleteMemberChatItem GroupId GroupMemberId ChatItemId
| APIChatItemReaction {chatRef :: ChatRef, chatItemId :: ChatItemId, add :: Bool, reaction :: MsgReaction} | APIChatItemReaction {chatRef :: ChatRef, chatItemId :: ChatItemId, add :: Bool, reaction :: MsgReaction}
| APIForwardChatItem {toChatRef :: ChatRef, fromChatRef :: ChatRef, chatItemId :: ChatItemId}
| APIUserRead UserId | APIUserRead UserId
| UserRead | UserRead
| APIChatRead ChatRef (Maybe (ChatItemId, ChatItemId)) | APIChatRead ChatRef (Maybe (ChatItemId, ChatItemId))
@ -408,6 +409,9 @@ data ChatCommand
| AddressAutoAccept (Maybe AutoAccept) | AddressAutoAccept (Maybe AutoAccept)
| AcceptContact IncognitoEnabled ContactName | AcceptContact IncognitoEnabled ContactName
| RejectContact ContactName | RejectContact ContactName
| ForwardMessage {toChatName :: ChatName, fromContactName :: ContactName, forwardedMsg :: Text}
| ForwardGroupMessage {toChatName :: ChatName, fromGroupName :: GroupName, fromMemberName_ :: Maybe ContactName, forwardedMsg :: Text}
| ForwardLocalMessage {toChatName :: ChatName, forwardedMsg :: Text}
| SendMessage ChatName Text | SendMessage ChatName Text
| SendMemberContactMessage GroupName ContactName Text | SendMemberContactMessage GroupName ContactName Text
| SendLiveMessage ChatName Text | SendLiveMessage ChatName Text
@ -1114,6 +1118,7 @@ data ChatErrorType
| CEFallbackToSMPProhibited {fileId :: FileTransferId} | CEFallbackToSMPProhibited {fileId :: FileTransferId}
| CEInlineFileProhibited {fileId :: FileTransferId} | CEInlineFileProhibited {fileId :: FileTransferId}
| CEInvalidQuote | CEInvalidQuote
| CEInvalidForward
| CEInvalidChatItemUpdate | CEInvalidChatItemUpdate
| CEInvalidChatItemDelete | CEInvalidChatItemDelete
| CEHasCurrentCall | CEHasCurrentCall

View file

@ -339,6 +339,7 @@ data CIMeta (c :: ChatType) (d :: MsgDirection) = CIMeta
itemText :: Text, itemText :: Text,
itemStatus :: CIStatus d, itemStatus :: CIStatus d,
itemSharedMsgId :: Maybe SharedMsgId, itemSharedMsgId :: Maybe SharedMsgId,
itemForwarded :: Maybe CIForwardedFrom,
itemDeleted :: Maybe (CIDeleted c), itemDeleted :: Maybe (CIDeleted c),
itemEdited :: Bool, itemEdited :: Bool,
itemTimed :: Maybe CITimed, itemTimed :: Maybe CITimed,
@ -350,15 +351,15 @@ data CIMeta (c :: ChatType) (d :: MsgDirection) = CIMeta
} }
deriving (Show) deriving (Show)
mkCIMeta :: forall c d. ChatTypeI c => ChatItemId -> CIContent d -> Text -> CIStatus d -> Maybe SharedMsgId -> Maybe (CIDeleted c) -> Bool -> Maybe CITimed -> Maybe Bool -> UTCTime -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> UTCTime -> CIMeta c d mkCIMeta :: forall c d. ChatTypeI c => ChatItemId -> CIContent d -> Text -> CIStatus d -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe (CIDeleted c) -> Bool -> Maybe CITimed -> Maybe Bool -> UTCTime -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> UTCTime -> CIMeta c d
mkCIMeta itemId itemContent itemText itemStatus itemSharedMsgId itemDeleted itemEdited itemTimed itemLive currentTs itemTs forwardedByMember createdAt updatedAt = mkCIMeta itemId itemContent itemText itemStatus itemSharedMsgId itemForwarded itemDeleted itemEdited itemTimed itemLive currentTs itemTs forwardedByMember createdAt updatedAt =
let editable = case itemContent of let editable = case itemContent of
CISndMsgContent _ -> CISndMsgContent _ ->
case chatTypeI @c of case chatTypeI @c of
SCTLocal -> isNothing itemDeleted SCTLocal -> isNothing itemDeleted && isNothing itemForwarded
_ -> diffUTCTime currentTs itemTs < nominalDay && isNothing itemDeleted _ -> diffUTCTime currentTs itemTs < nominalDay && isNothing itemDeleted && isNothing itemForwarded
_ -> False _ -> False
in CIMeta {itemId, itemTs, itemText, itemStatus, itemSharedMsgId, itemDeleted, itemEdited, itemTimed, itemLive, editable, forwardedByMember, createdAt, updatedAt} in CIMeta {itemId, itemTs, itemText, itemStatus, itemSharedMsgId, itemForwarded, itemDeleted, itemEdited, itemTimed, itemLive, editable, forwardedByMember, createdAt, updatedAt}
dummyMeta :: ChatItemId -> UTCTime -> Text -> CIMeta c 'MDSnd dummyMeta :: ChatItemId -> UTCTime -> Text -> CIMeta c 'MDSnd
dummyMeta itemId ts itemText = dummyMeta itemId ts itemText =
@ -368,6 +369,7 @@ dummyMeta itemId ts itemText =
itemText, itemText,
itemStatus = CISSndNew, itemStatus = CISSndNew,
itemSharedMsgId = Nothing, itemSharedMsgId = Nothing,
itemForwarded = Nothing,
itemDeleted = Nothing, itemDeleted = Nothing,
itemEdited = False, itemEdited = False,
itemTimed = Nothing, itemTimed = Nothing,
@ -548,6 +550,21 @@ ciFileEnded = \case
CIFSRcvError -> True CIFSRcvError -> True
CIFSInvalid {} -> True CIFSInvalid {} -> True
ciFileLoaded :: CIFileStatus d -> Bool
ciFileLoaded = \case
CIFSSndStored -> True
CIFSSndTransfer {} -> True
CIFSSndComplete -> True
CIFSSndCancelled -> True
CIFSSndError -> True
CIFSRcvInvitation -> False
CIFSRcvAccepted -> False
CIFSRcvTransfer {} -> False
CIFSRcvCancelled -> False
CIFSRcvComplete -> True
CIFSRcvError -> False
CIFSInvalid {} -> False
data ACIFileStatus = forall d. MsgDirectionI d => AFS (SMsgDirection d) (CIFileStatus d) data ACIFileStatus = forall d. MsgDirectionI d => AFS (SMsgDirection d) (CIFileStatus d)
deriving instance Show ACIFileStatus deriving instance Show ACIFileStatus
@ -981,11 +998,43 @@ itemDeletedTs = \case
CIBlockedByAdmin ts -> ts CIBlockedByAdmin ts -> ts
CIModerated ts _ -> ts CIModerated ts _ -> ts
data CIForwardedFrom
= CIFFUnknown
| CIFFContact {chatName :: Text, msgDir :: MsgDirection, contactId :: Maybe ContactId, chatItemId :: Maybe ChatItemId}
| CIFFGroup {chatName :: Text, msgDir :: MsgDirection, groupId :: Maybe GroupId, chatItemId :: Maybe ChatItemId}
deriving (Show)
cmForwardedFrom :: AChatMsgEvent -> Maybe CIForwardedFrom
cmForwardedFrom = \case
ACME _ (XMsgNew (MCForward _)) -> Just CIFFUnknown
_ -> Nothing
data CIForwardedFromTag
= CIFFUnknown_
| CIFFContact_
| CIFFGroup_
instance FromField CIForwardedFromTag where fromField = fromTextField_ textDecode
instance ToField CIForwardedFromTag where toField = toField . textEncode
instance TextEncoding CIForwardedFromTag where
textDecode = \case
"unknown" -> Just CIFFUnknown_
"contact" -> Just CIFFContact_
"group" -> Just CIFFGroup_
_ -> Nothing
textEncode = \case
CIFFUnknown_ -> "unknown"
CIFFContact_ -> "contact"
CIFFGroup_ -> "group"
data ChatItemInfo = ChatItemInfo data ChatItemInfo = ChatItemInfo
{ itemVersions :: [ChatItemVersion], { itemVersions :: [ChatItemVersion],
memberDeliveryStatuses :: Maybe [MemberDeliveryStatus] memberDeliveryStatuses :: Maybe [MemberDeliveryStatus],
forwardedFromChatItem :: Maybe AChatItem
} }
deriving (Eq, Show) deriving (Show)
data ChatItemVersion = ChatItemVersion data ChatItemVersion = ChatItemVersion
{ chatItemVersionId :: Int64, { chatItemVersionId :: Int64,
@ -1043,6 +1092,8 @@ instance ChatTypeI c => ToJSON (CIDeleted c) where
toJSON = J.toJSON . jsonCIDeleted toJSON = J.toJSON . jsonCIDeleted
toEncoding = J.toEncoding . jsonCIDeleted toEncoding = J.toEncoding . jsonCIDeleted
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CIFF") ''CIForwardedFrom)
$(JQ.deriveJSON defaultJSON ''CITimed) $(JQ.deriveJSON defaultJSON ''CITimed)
$(JQ.deriveJSON (enumJSON $ dropPrefix "SSP") ''SndCIStatusProgress) $(JQ.deriveJSON (enumJSON $ dropPrefix "SSP") ''SndCIStatusProgress)
@ -1066,8 +1117,6 @@ $(JQ.deriveJSON defaultJSON ''MemberDeliveryStatus)
$(JQ.deriveJSON defaultJSON ''ChatItemVersion) $(JQ.deriveJSON defaultJSON ''ChatItemVersion)
$(JQ.deriveJSON defaultJSON ''ChatItemInfo)
instance (ChatTypeI c, MsgDirectionI d) => FromJSON (CIMeta c d) where instance (ChatTypeI c, MsgDirectionI d) => FromJSON (CIMeta c d) where
parseJSON = $(JQ.mkParseJSON defaultJSON ''CIMeta) parseJSON = $(JQ.mkParseJSON defaultJSON ''CIMeta)
@ -1157,6 +1206,8 @@ instance ChatTypeI c => ToJSON (CChatItem c) where
toJSON (CChatItem _ ci) = J.toJSON ci toJSON (CChatItem _ ci) = J.toJSON ci
toEncoding (CChatItem _ ci) = J.toEncoding ci toEncoding (CChatItem _ ci) = J.toEncoding ci
$(JQ.deriveJSON defaultJSON ''ChatItemInfo)
$(JQ.deriveJSON defaultJSON ''ChatStats) $(JQ.deriveJSON defaultJSON ''ChatStats)
instance ChatTypeI c => ToJSON (Chat c) where instance ChatTypeI c => ToJSON (Chat c) where

View file

@ -43,6 +43,8 @@ $(JQ.deriveJSON (enumJSON $ dropPrefix "MD") ''MsgDirection)
instance FromField AMsgDirection where fromField = fromIntField_ $ fmap fromMsgDirection . msgDirectionIntP instance FromField AMsgDirection where fromField = fromIntField_ $ fmap fromMsgDirection . msgDirectionIntP
instance FromField MsgDirection where fromField = fromIntField_ msgDirectionIntP
instance ToField MsgDirection where toField = toField . msgDirectionInt instance ToField MsgDirection where toField = toField . msgDirectionInt
data SMsgDirection (d :: MsgDirection) where data SMsgDirection (d :: MsgDirection) where

View file

@ -0,0 +1,36 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Migrations.M20240402_item_forwarded where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20240402_item_forwarded :: Query
m20240402_item_forwarded =
[sql|
ALTER TABLE chat_items ADD COLUMN fwd_from_tag TEXT;
ALTER TABLE chat_items ADD COLUMN fwd_from_chat_name TEXT;
ALTER TABLE chat_items ADD COLUMN fwd_from_msg_dir INTEGER;
ALTER TABLE chat_items ADD COLUMN fwd_from_contact_id INTEGER REFERENCES contacts ON DELETE SET NULL;
ALTER TABLE chat_items ADD COLUMN fwd_from_group_id INTEGER REFERENCES groups ON DELETE SET NULL;
ALTER TABLE chat_items ADD COLUMN fwd_from_chat_item_id INTEGER REFERENCES chat_items ON DELETE SET NULL;
CREATE INDEX idx_chat_items_fwd_from_contact_id ON chat_items(fwd_from_contact_id);
CREATE INDEX idx_chat_items_fwd_from_group_id ON chat_items(fwd_from_group_id);
CREATE INDEX idx_chat_items_fwd_from_chat_item_id ON chat_items(fwd_from_chat_item_id);
|]
down_m20240402_item_forwarded :: Query
down_m20240402_item_forwarded =
[sql|
DROP INDEX idx_chat_items_fwd_from_contact_id;
DROP INDEX idx_chat_items_fwd_from_group_id;
DROP INDEX idx_chat_items_fwd_from_chat_item_id;
ALTER TABLE chat_items DROP COLUMN fwd_from_tag;
ALTER TABLE chat_items DROP COLUMN fwd_from_chat_name;
ALTER TABLE chat_items DROP COLUMN fwd_from_msg_dir;
ALTER TABLE chat_items DROP COLUMN fwd_from_contact_id;
ALTER TABLE chat_items DROP COLUMN fwd_from_group_id;
ALTER TABLE chat_items DROP COLUMN fwd_from_chat_item_id;
|]

View file

@ -382,7 +382,13 @@ CREATE TABLE chat_items(
item_deleted_ts TEXT, item_deleted_ts TEXT,
forwarded_by_group_member_id INTEGER REFERENCES group_members ON DELETE SET NULL, forwarded_by_group_member_id INTEGER REFERENCES group_members ON DELETE SET NULL,
item_content_tag TEXT, item_content_tag TEXT,
note_folder_id INTEGER DEFAULT NULL REFERENCES note_folders ON DELETE CASCADE note_folder_id INTEGER DEFAULT NULL REFERENCES note_folders ON DELETE CASCADE,
fwd_from_tag TEXT,
fwd_from_chat_name TEXT,
fwd_from_msg_dir INTEGER,
fwd_from_contact_id INTEGER REFERENCES contacts ON DELETE SET NULL,
fwd_from_group_id INTEGER REFERENCES groups ON DELETE SET NULL,
fwd_from_chat_item_id INTEGER REFERENCES chat_items ON DELETE SET NULL
); );
CREATE TABLE chat_item_messages( CREATE TABLE chat_item_messages(
chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE, chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE,
@ -860,3 +866,10 @@ CREATE INDEX idx_chat_items_notes_item_status on chat_items(
item_status item_status
); );
CREATE INDEX idx_files_redirect_file_id on files(redirect_file_id); CREATE INDEX idx_files_redirect_file_id on files(redirect_file_id);
CREATE INDEX idx_chat_items_fwd_from_contact_id ON chat_items(
fwd_from_contact_id
);
CREATE INDEX idx_chat_items_fwd_from_group_id ON chat_items(fwd_from_group_id);
CREATE INDEX idx_chat_items_fwd_from_chat_item_id ON chat_items(
fwd_from_chat_item_id
);

View file

@ -588,6 +588,7 @@ parseMsgContainer :: J.Object -> JT.Parser MsgContainer
parseMsgContainer v = parseMsgContainer v =
MCQuote <$> v .: "quote" <*> mc MCQuote <$> v .: "quote" <*> mc
<|> (v .: "forward" >>= \f -> (if f then MCForward else MCSimple) <$> mc) <|> (v .: "forward" >>= \f -> (if f then MCForward else MCSimple) <$> mc)
<|> (MCForward <$> ((v .: "forward" :: JT.Parser J.Object) *> mc))
<|> MCSimple <$> mc <|> MCSimple <$> mc
where where
mc = ExtMsgContent <$> v .: "content" <*> v .:? "file" <*> v .:? "ttl" <*> v .:? "live" mc = ExtMsgContent <$> v .: "content" <*> v .:? "file" <*> v .:? "ttl" <*> v .:? "live"

View file

@ -145,8 +145,8 @@ import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, MsgMeta (..), UserI
import Simplex.Messaging.Agent.Store.SQLite (firstRow, firstRow', maybeFirstRow) import Simplex.Messaging.Agent.Store.SQLite (firstRow, firstRow', maybeFirstRow)
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
import qualified Simplex.Messaging.Crypto as C import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.Ratchet (PQSupport)
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
import Simplex.Messaging.Crypto.Ratchet (PQSupport)
import Simplex.Messaging.Util (eitherToMaybe) import Simplex.Messaging.Util (eitherToMaybe)
import UnliftIO.STM import UnliftIO.STM
@ -330,9 +330,9 @@ updateChatTs db User {userId} chatDirection chatTs = case toChatInfo chatDirecti
(chatTs, userId, noteFolderId) (chatTs, userId, noteFolderId)
_ -> pure () _ -> pure ()
createNewSndChatItem :: DB.Connection -> User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> Maybe (CIQuote c) -> Maybe CITimed -> Bool -> UTCTime -> IO ChatItemId createNewSndChatItem :: DB.Connection -> User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> Maybe (CIQuote c) -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> UTCTime -> IO ChatItemId
createNewSndChatItem db user chatDirection SndMessage {msgId, sharedMsgId} ciContent quotedItem timed live createdAt = createNewSndChatItem db user chatDirection SndMessage {msgId, sharedMsgId} ciContent quotedItem itemForwarded timed live createdAt =
createNewChatItem_ db user chatDirection createdByMsgId (Just sharedMsgId) ciContent quoteRow timed live createdAt Nothing createdAt createNewChatItem_ db user chatDirection createdByMsgId (Just sharedMsgId) ciContent quoteRow itemForwarded timed live createdAt Nothing createdAt
where where
createdByMsgId = if msgId == 0 then Nothing else Just msgId createdByMsgId = if msgId == 0 then Nothing else Just msgId
quoteRow :: NewQuoteRow quoteRow :: NewQuoteRow
@ -346,12 +346,13 @@ createNewSndChatItem db user chatDirection SndMessage {msgId, sharedMsgId} ciCon
CIQGroupRcv (Just GroupMember {memberId}) -> (Just False, Just memberId) CIQGroupRcv (Just GroupMember {memberId}) -> (Just False, Just memberId)
CIQGroupRcv Nothing -> (Just False, Nothing) CIQGroupRcv Nothing -> (Just False, Nothing)
createNewRcvChatItem :: ChatTypeQuotable c => DB.Connection -> User -> ChatDirection c 'MDRcv -> RcvMessage -> Maybe SharedMsgId -> CIContent 'MDRcv -> Maybe CITimed -> Bool -> UTCTime -> UTCTime -> IO (ChatItemId, Maybe (CIQuote c)) createNewRcvChatItem :: ChatTypeQuotable c => DB.Connection -> User -> ChatDirection c 'MDRcv -> RcvMessage -> Maybe SharedMsgId -> CIContent 'MDRcv -> Maybe CITimed -> Bool -> UTCTime -> UTCTime -> IO (ChatItemId, Maybe (CIQuote c), Maybe CIForwardedFrom)
createNewRcvChatItem db user chatDirection RcvMessage {msgId, chatMsgEvent, forwardedByMember} sharedMsgId_ ciContent timed live itemTs createdAt = do createNewRcvChatItem db user chatDirection RcvMessage {msgId, chatMsgEvent, forwardedByMember} sharedMsgId_ ciContent timed live itemTs createdAt = do
ciId <- createNewChatItem_ db user chatDirection (Just msgId) sharedMsgId_ ciContent quoteRow timed live itemTs forwardedByMember createdAt ciId <- createNewChatItem_ db user chatDirection (Just msgId) sharedMsgId_ ciContent quoteRow itemForwarded timed live itemTs forwardedByMember createdAt
quotedItem <- mapM (getChatItemQuote_ db user chatDirection) quotedMsg quotedItem <- mapM (getChatItemQuote_ db user chatDirection) quotedMsg
pure (ciId, quotedItem) pure (ciId, quotedItem, itemForwarded)
where where
itemForwarded = cmForwardedFrom chatMsgEvent
quotedMsg = cmToQuotedMsg chatMsgEvent quotedMsg = cmToQuotedMsg chatMsgEvent
quoteRow :: NewQuoteRow quoteRow :: NewQuoteRow
quoteRow = case quotedMsg of quoteRow = case quotedMsg of
@ -364,13 +365,13 @@ createNewRcvChatItem db user chatDirection RcvMessage {msgId, chatMsgEvent, forw
createNewChatItemNoMsg :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> CIContent d -> UTCTime -> UTCTime -> IO ChatItemId createNewChatItemNoMsg :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> CIContent d -> UTCTime -> UTCTime -> IO ChatItemId
createNewChatItemNoMsg db user chatDirection ciContent itemTs = createNewChatItemNoMsg db user chatDirection ciContent itemTs =
createNewChatItem_ db user chatDirection Nothing Nothing ciContent quoteRow Nothing False itemTs Nothing createNewChatItem_ db user chatDirection Nothing Nothing ciContent quoteRow Nothing Nothing False itemTs Nothing
where where
quoteRow :: NewQuoteRow quoteRow :: NewQuoteRow
quoteRow = (Nothing, Nothing, Nothing, Nothing, Nothing) quoteRow = (Nothing, Nothing, Nothing, Nothing, Nothing)
createNewChatItem_ :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> Maybe MessageId -> Maybe SharedMsgId -> CIContent d -> NewQuoteRow -> Maybe CITimed -> Bool -> UTCTime -> Maybe GroupMemberId -> UTCTime -> IO ChatItemId createNewChatItem_ :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> Maybe MessageId -> Maybe SharedMsgId -> CIContent d -> NewQuoteRow -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> UTCTime -> Maybe GroupMemberId -> UTCTime -> IO ChatItemId
createNewChatItem_ db User {userId} chatDirection msgId_ sharedMsgId ciContent quoteRow timed live itemTs forwardedByMember createdAt = do createNewChatItem_ db User {userId} chatDirection msgId_ sharedMsgId ciContent quoteRow itemForwarded timed live itemTs forwardedByMember createdAt = do
DB.execute DB.execute
db db
[sql| [sql|
@ -381,10 +382,12 @@ createNewChatItem_ db User {userId} chatDirection msgId_ sharedMsgId ciContent q
item_sent, item_ts, item_content, item_content_tag, item_text, item_status, shared_msg_id, item_sent, item_ts, item_content, item_content_tag, item_text, item_status, shared_msg_id,
forwarded_by_group_member_id, created_at, updated_at, item_live, timed_ttl, timed_delete_at, forwarded_by_group_member_id, created_at, updated_at, item_live, timed_ttl, timed_delete_at,
-- quote -- quote
quoted_shared_msg_id, quoted_sent_at, quoted_content, quoted_sent, quoted_member_id quoted_shared_msg_id, quoted_sent_at, quoted_content, quoted_sent, quoted_member_id,
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) -- forwarded from
fwd_from_tag, fwd_from_chat_name, fwd_from_msg_dir, fwd_from_contact_id, fwd_from_group_id, fwd_from_chat_item_id
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|] |]
((userId, msgId_) :. idsRow :. itemRow :. quoteRow) ((userId, msgId_) :. idsRow :. itemRow :. quoteRow :. forwardedFromRow)
ciId <- insertedRowId db ciId <- insertedRowId db
forM_ msgId_ $ \msgId -> insertChatItemMessage_ db ciId msgId createdAt forM_ msgId_ $ \msgId -> insertChatItemMessage_ db ciId msgId createdAt
pure ciId pure ciId
@ -399,6 +402,16 @@ createNewChatItem_ db User {userId} chatDirection msgId_ sharedMsgId ciContent q
CDGroupSnd GroupInfo {groupId} -> (Nothing, Just groupId, Nothing, Nothing) CDGroupSnd GroupInfo {groupId} -> (Nothing, Just groupId, Nothing, Nothing)
CDLocalRcv NoteFolder {noteFolderId} -> (Nothing, Nothing, Nothing, Just noteFolderId) CDLocalRcv NoteFolder {noteFolderId} -> (Nothing, Nothing, Nothing, Just noteFolderId)
CDLocalSnd NoteFolder {noteFolderId} -> (Nothing, Nothing, Nothing, Just noteFolderId) CDLocalSnd NoteFolder {noteFolderId} -> (Nothing, Nothing, Nothing, Just noteFolderId)
forwardedFromRow :: (Maybe CIForwardedFromTag, Maybe Text, Maybe MsgDirection, Maybe Int64, Maybe Int64, Maybe Int64)
forwardedFromRow = case itemForwarded of
Nothing ->
(Nothing, Nothing, Nothing, Nothing, Nothing, Nothing)
Just CIFFUnknown ->
(Just CIFFUnknown_, Nothing, Nothing, Nothing, Nothing, Nothing)
Just CIFFContact {chatName, msgDir, contactId, chatItemId} ->
(Just CIFFContact_, Just chatName, Just msgDir, contactId, Nothing, chatItemId)
Just CIFFGroup {chatName, msgDir, groupId, chatItemId} ->
(Just CIFFContact_, Just chatName, Just msgDir, Nothing, groupId, chatItemId)
ciTimedRow :: Maybe CITimed -> (Maybe Int, Maybe UTCTime) ciTimedRow :: Maybe CITimed -> (Maybe Int, Maybe UTCTime)
ciTimedRow (Just CITimed {ttl, deleteAt}) = (Just ttl, deleteAt) ciTimedRow (Just CITimed {ttl, deleteAt}) = (Just ttl, deleteAt)
@ -794,7 +807,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 -- this function can be changed so it never fails, not only avoid failure on invalid json
toLocalChatItem :: UTCTime -> ChatItemRow -> Either StoreError (CChatItem 'CTLocal) toLocalChatItem :: UTCTime -> ChatItemRow -> Either StoreError (CChatItem 'CTLocal)
toLocalChatItem currentTs ((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) = toLocalChatItem currentTs ((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) =
chatItem $ fromRight invalid $ dbParseACIContent itemContentText chatItem $ fromRight invalid $ dbParseACIContent itemContentText
where where
invalid = ACIContent msgDir $ CIInvalidJSON itemContentText invalid = ACIContent msgDir $ CIInvalidJSON itemContentText
@ -826,7 +839,8 @@ toLocalChatItem currentTs ((itemId, itemTs, AMsgDirection msgDir, itemContentTex
DBCINotDeleted -> Nothing DBCINotDeleted -> Nothing
_ -> Just (CIDeleted @CTLocal deletedTs) _ -> Just (CIDeleted @CTLocal deletedTs)
itemEdited' = fromMaybe False itemEdited itemEdited' = fromMaybe False itemEdited
in mkCIMeta itemId content itemText status sharedMsgId itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs Nothing createdAt updatedAt itemForwarded = toCIForwardedFrom forwardedFromRow
in mkCIMeta itemId content itemText status sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs Nothing createdAt updatedAt
ciTimed :: Maybe CITimed ciTimed :: Maybe CITimed
ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt} ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt}
@ -1391,7 +1405,14 @@ type MaybeCIFIleRow = (Maybe Int64, Maybe String, Maybe Integer, Maybe FilePath,
type ChatItemModeRow = (Maybe Int, Maybe UTCTime, Maybe Bool) type ChatItemModeRow = (Maybe Int, Maybe UTCTime, Maybe Bool)
type ChatItemRow = (Int64, ChatItemTs, AMsgDirection, Text, Text, ACIStatus, Maybe SharedMsgId) :. (Int, Maybe UTCTime, Maybe Bool, UTCTime, UTCTime) :. ChatItemModeRow :. MaybeCIFIleRow type ChatItemForwardedFromRow = (Maybe CIForwardedFromTag, Maybe Text, Maybe MsgDirection, Maybe Int64, Maybe Int64, Maybe Int64)
type ChatItemRow =
(Int64, ChatItemTs, AMsgDirection, Text, Text, ACIStatus, Maybe SharedMsgId)
:. (Int, Maybe UTCTime, Maybe Bool, UTCTime, UTCTime)
:. ChatItemForwardedFromRow
:. ChatItemModeRow
:. MaybeCIFIleRow
type QuoteRow = (Maybe ChatItemId, Maybe SharedMsgId, Maybe UTCTime, Maybe MsgContent, Maybe Bool) type QuoteRow = (Maybe ChatItemId, Maybe SharedMsgId, Maybe UTCTime, Maybe MsgContent, Maybe Bool)
@ -1406,7 +1427,7 @@ toQuote (quotedItemId, quotedSharedMsgId, quotedSentAt, quotedMsgContent, _) dir
-- this function can be changed so it never fails, not only avoid failure on invalid json -- 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 :: UTCTime -> ChatItemRow :. QuoteRow -> Either StoreError (CChatItem 'CTDirect)
toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. quoteRow) = toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. quoteRow) =
chatItem $ fromRight invalid $ dbParseACIContent itemContentText chatItem $ fromRight invalid $ dbParseACIContent itemContentText
where where
invalid = ACIContent msgDir $ CIInvalidJSON itemContentText invalid = ACIContent msgDir $ CIInvalidJSON itemContentText
@ -1438,10 +1459,19 @@ toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentT
DBCINotDeleted -> Nothing DBCINotDeleted -> Nothing
_ -> Just (CIDeleted @CTDirect deletedTs) _ -> Just (CIDeleted @CTDirect deletedTs)
itemEdited' = fromMaybe False itemEdited itemEdited' = fromMaybe False itemEdited
in mkCIMeta itemId content itemText status sharedMsgId itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs Nothing createdAt updatedAt itemForwarded = toCIForwardedFrom forwardedFromRow
in mkCIMeta itemId content itemText status sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs Nothing createdAt updatedAt
ciTimed :: Maybe CITimed ciTimed :: Maybe CITimed
ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt} ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt}
toCIForwardedFrom :: ChatItemForwardedFromRow -> Maybe CIForwardedFrom
toCIForwardedFrom (fwdFromTag, fwdFromChatName, fwdFromMsgDir, fwdFromContactId, fwdFromGroupId, fwdFromChatItemId) =
case (fwdFromTag, fwdFromChatName, fwdFromMsgDir, fwdFromContactId, fwdFromGroupId, fwdFromChatItemId) of
(Just CIFFUnknown_, Nothing, Nothing, Nothing, Nothing, Nothing) -> Just CIFFUnknown
(Just CIFFContact_, Just chatName, Just msgDir, contactId, Nothing, chatId) -> Just $ CIFFContact chatName msgDir contactId chatId
(Just CIFFGroup_, Just chatName, Just msgDir, Nothing, groupId, chatId) -> Just $ CIFFGroup chatName msgDir groupId chatId
_ -> Nothing
type GroupQuoteRow = QuoteRow :. MaybeGroupMemberRow type GroupQuoteRow = QuoteRow :. MaybeGroupMemberRow
toGroupQuote :: QuoteRow -> Maybe GroupMember -> Maybe (CIQuote 'CTGroup) toGroupQuote :: QuoteRow -> Maybe GroupMember -> Maybe (CIQuote 'CTGroup)
@ -1454,7 +1484,7 @@ toGroupQuote qr@(_, _, _, _, quotedSent) quotedMember_ = toQuote qr $ direction
-- this function can be changed so it never fails, not only avoid failure on invalid json -- this function can be changed so it never fails, not only avoid failure on invalid json
toGroupChatItem :: UTCTime -> Int64 -> ChatItemRow :. Only (Maybe GroupMemberId) :. MaybeGroupMemberRow :. GroupQuoteRow :. MaybeGroupMemberRow -> Either StoreError (CChatItem 'CTGroup) toGroupChatItem :: UTCTime -> Int64 -> ChatItemRow :. Only (Maybe GroupMemberId) :. MaybeGroupMemberRow :. GroupQuoteRow :. MaybeGroupMemberRow -> Either StoreError (CChatItem 'CTGroup)
toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. Only forwardedByMember :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_) = do toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. Only forwardedByMember :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_) = do
chatItem $ fromRight invalid $ dbParseACIContent itemContentText chatItem $ fromRight invalid $ dbParseACIContent itemContentText
where where
member_ = toMaybeGroupMember userContactId memberRow_ member_ = toMaybeGroupMember userContactId memberRow_
@ -1491,7 +1521,8 @@ toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir,
DBCIBlockedByAdmin -> Just (CIBlockedByAdmin deletedTs) DBCIBlockedByAdmin -> Just (CIBlockedByAdmin deletedTs)
_ -> Just (maybe (CIDeleted @CTGroup deletedTs) (CIModerated deletedTs) deletedByGroupMember_) _ -> Just (maybe (CIDeleted @CTGroup deletedTs) (CIModerated deletedTs) deletedByGroupMember_)
itemEdited' = fromMaybe False itemEdited itemEdited' = fromMaybe False itemEdited
in mkCIMeta itemId content itemText status sharedMsgId itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs forwardedByMember createdAt updatedAt itemForwarded = toCIForwardedFrom forwardedFromRow
in mkCIMeta itemId content itemText status sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs forwardedByMember createdAt updatedAt
ciTimed :: Maybe CITimed ciTimed :: Maybe CITimed
ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt} ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt}
@ -1726,7 +1757,10 @@ getDirectChatItem db User {userId} contactId itemId = ExceptT $ do
[sql| [sql|
SELECT SELECT
-- ChatItem -- ChatItem
i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live, i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id,
i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at,
i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id,
i.timed_ttl, i.timed_delete_at, i.item_live,
-- CIFile -- 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_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol,
-- DirectQuote -- DirectQuote
@ -1966,7 +2000,10 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do
[sql| [sql|
SELECT SELECT
-- ChatItem -- ChatItem
i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live, i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id,
i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at,
i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id,
i.timed_ttl, i.timed_delete_at, i.item_live,
-- CIFile -- 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_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol,
-- CIMeta forwardedByMember -- CIMeta forwardedByMember
@ -2067,7 +2104,10 @@ getLocalChatItem db User {userId} folderId itemId = ExceptT $ do
[sql| [sql|
SELECT SELECT
-- ChatItem -- ChatItem
i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live, i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id,
i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at,
i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id,
i.timed_ttl, i.timed_delete_at, i.item_live,
-- CIFile -- 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_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol
FROM chat_items i FROM chat_items i

View file

@ -104,6 +104,7 @@ import Simplex.Chat.Migrations.M20240226_users_restrict
import Simplex.Chat.Migrations.M20240228_pq import Simplex.Chat.Migrations.M20240228_pq
import Simplex.Chat.Migrations.M20240313_drop_agent_ack_cmd_id import Simplex.Chat.Migrations.M20240313_drop_agent_ack_cmd_id
import Simplex.Chat.Migrations.M20240324_custom_data import Simplex.Chat.Migrations.M20240324_custom_data
import Simplex.Chat.Migrations.M20240402_item_forwarded
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)] schemaMigrations :: [(String, Query, Maybe Query)]
@ -207,7 +208,8 @@ schemaMigrations =
("20240226_users_restrict", m20240226_users_restrict, Just down_m20240226_users_restrict), ("20240226_users_restrict", m20240226_users_restrict, Just down_m20240226_users_restrict),
("20240228_pq", m20240228_pq, Just down_m20240228_pq), ("20240228_pq", m20240228_pq, Just down_m20240228_pq),
("20240313_drop_agent_ack_cmd_id", m20240313_drop_agent_ack_cmd_id, Just down_m20240313_drop_agent_ack_cmd_id), ("20240313_drop_agent_ack_cmd_id", m20240313_drop_agent_ack_cmd_id, Just down_m20240313_drop_agent_ack_cmd_id),
("20240324_custom_data", m20240324_custom_data, Just down_m20240324_custom_data) ("20240324_custom_data", m20240324_custom_data, Just down_m20240324_custom_data),
("20240402_item_forwarded", m20240402_item_forwarded, Just down_m20240402_item_forwarded)
] ]
-- | The list of migrations in ascending order by date -- | The list of migrations in ascending order by date

View file

@ -86,7 +86,10 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do
Right SendLiveMessage {} -> True Right SendLiveMessage {} -> True
Right SendFile {} -> True Right SendFile {} -> True
Right SendMessageQuote {} -> True Right SendMessageQuote {} -> True
Right ForwardMessage {} -> True
Right ForwardLocalMessage {} -> True
Right SendGroupMessageQuote {} -> True Right SendGroupMessageQuote {} -> True
Right ForwardGroupMessage {} -> True
Right SendMessageBroadcast {} -> True Right SendMessageBroadcast {} -> True
_ -> False _ -> False
startLiveMessage :: Either a ChatCommand -> ChatResponse -> IO () startLiveMessage :: Either a ChatCommand -> ChatResponse -> IO ()

View file

@ -536,60 +536,68 @@ viewChats ts tz = concatMap chatPreview . reverse
_ -> [] _ -> []
viewChatItem :: forall c d. MsgDirectionI d => ChatInfo c -> ChatItem c d -> Bool -> CurrentTime -> TimeZone -> [StyledString] viewChatItem :: forall c d. MsgDirectionI d => ChatInfo c -> ChatItem c d -> Bool -> CurrentTime -> TimeZone -> [StyledString]
viewChatItem chat ci@ChatItem {chatDir, meta = meta@CIMeta {forwardedByMember}, content, quotedItem, file} doShow ts tz = viewChatItem chat ci@ChatItem {chatDir, meta = meta@CIMeta {itemForwarded, forwardedByMember}, content, quotedItem, file} doShow ts tz =
withGroupMsgForwarded . withItemDeleted <$> viewCI withGroupMsgForwarded . withItemDeleted <$> viewCI
where where
viewCI = case chat of viewCI = case chat of
DirectChat c -> case chatDir of DirectChat c -> case chatDir of
CIDirectSnd -> case content of CIDirectSnd -> case content of
CISndMsgContent mc -> hideLive meta $ withSndFile to $ sndMsg to quote mc CISndMsgContent mc -> hideLive meta $ withSndFile to $ sndMsg to context mc
CISndGroupEvent {} -> showSndItemProhibited to CISndGroupEvent {} -> showSndItemProhibited to
_ -> showSndItem to _ -> showSndItem to
where where
to = ttyToContact' c to = ttyToContact' c
CIDirectRcv -> case content of CIDirectRcv -> case content of
CIRcvMsgContent mc -> withRcvFile from $ rcvMsg from quote mc CIRcvMsgContent mc -> withRcvFile from $ rcvMsg from context mc
CIRcvIntegrityError err -> viewRcvIntegrityError from err ts tz meta CIRcvIntegrityError err -> viewRcvIntegrityError from err ts tz meta
CIRcvGroupEvent {} -> showRcvItemProhibited from CIRcvGroupEvent {} -> showRcvItemProhibited from
_ -> showRcvItem from _ -> showRcvItem from
where where
from = ttyFromContact c from = ttyFromContact c
where where
quote = maybe [] (directQuote chatDir) quotedItem context =
maybe
(maybe [] forwardedFrom itemForwarded)
(directQuote chatDir)
quotedItem
GroupChat g -> case chatDir of GroupChat g -> case chatDir of
CIGroupSnd -> case content of CIGroupSnd -> case content of
CISndMsgContent mc -> hideLive meta $ withSndFile to $ sndMsg to quote mc CISndMsgContent mc -> hideLive meta $ withSndFile to $ sndMsg to context mc
CISndGroupInvitation {} -> showSndItemProhibited to CISndGroupInvitation {} -> showSndItemProhibited to
_ -> showSndItem to _ -> showSndItem to
where where
to = ttyToGroup g to = ttyToGroup g
CIGroupRcv m -> case content of CIGroupRcv m -> case content of
CIRcvMsgContent mc -> withRcvFile from $ rcvMsg from quote mc CIRcvMsgContent mc -> withRcvFile from $ rcvMsg from context mc
CIRcvIntegrityError err -> viewRcvIntegrityError from err ts tz meta CIRcvIntegrityError err -> viewRcvIntegrityError from err ts tz meta
CIRcvGroupInvitation {} -> showRcvItemProhibited from CIRcvGroupInvitation {} -> showRcvItemProhibited from
CIRcvModerated {} -> receivedWithTime_ ts tz (ttyFromGroup g m) quote meta [plainContent content] False CIRcvModerated {} -> receivedWithTime_ ts tz (ttyFromGroup g m) context meta [plainContent content] False
CIRcvBlocked {} -> receivedWithTime_ ts tz (ttyFromGroup g m) quote meta [plainContent content] False CIRcvBlocked {} -> receivedWithTime_ ts tz (ttyFromGroup g m) context meta [plainContent content] False
_ -> showRcvItem from _ -> showRcvItem from
where where
from = ttyFromGroup g m from = ttyFromGroup g m
where where
quote = maybe [] (groupQuote g) quotedItem context =
maybe
(maybe [] forwardedFrom itemForwarded)
(groupQuote g)
quotedItem
LocalChat _ -> case chatDir of LocalChat _ -> case chatDir of
CILocalSnd -> case content of CILocalSnd -> case content of
CISndMsgContent mc -> hideLive meta $ withLocalFile to $ sndMsg to quote mc CISndMsgContent mc -> hideLive meta $ withLocalFile to $ sndMsg to context mc
CISndGroupEvent {} -> showSndItemProhibited to CISndGroupEvent {} -> showSndItemProhibited to
_ -> showSndItem to _ -> showSndItem to
where where
to = "* " to = "* "
CILocalRcv -> case content of CILocalRcv -> case content of
CIRcvMsgContent mc -> withLocalFile from $ rcvMsg from quote mc CIRcvMsgContent mc -> withLocalFile from $ rcvMsg from context mc
CIRcvIntegrityError err -> viewRcvIntegrityError from err ts tz meta CIRcvIntegrityError err -> viewRcvIntegrityError from err ts tz meta
CIRcvGroupEvent {} -> showRcvItemProhibited from CIRcvGroupEvent {} -> showRcvItemProhibited from
_ -> showRcvItem from _ -> showRcvItem from
where where
from = "* " from = "* "
where where
quote = [] context = maybe [] forwardedFrom itemForwarded
ContactRequest {} -> [] ContactRequest {} -> []
ContactConnection {} -> [] ContactConnection {} -> []
withItemDeleted item = case chatItemDeletedText ci (chatInfoMembership chat) of withItemDeleted item = case chatItemDeletedText ci (chatInfoMembership chat) of
@ -604,10 +612,10 @@ viewChatItem chat ci@ChatItem {chatDir, meta = meta@CIMeta {forwardedByMember},
withFile view dir l = maybe l (\f -> l <> view dir f ts tz meta) file withFile view dir l = maybe l (\f -> l <> view dir f ts tz meta) file
sndMsg = msg viewSentMessage sndMsg = msg viewSentMessage
rcvMsg = msg viewReceivedMessage rcvMsg = msg viewReceivedMessage
msg view dir quote mc = case (msgContentText mc, file, quote) of msg view dir context mc = case (msgContentText mc, file, context) of
("", Just _, []) -> [] ("", Just _, []) -> []
("", Just CIFile {fileName}, _) -> view dir quote (MCText $ T.pack fileName) ts tz meta ("", Just CIFile {fileName}, _) -> view dir context (MCText $ T.pack fileName) ts tz meta
_ -> view dir quote mc ts tz meta _ -> view dir context mc ts tz meta
showSndItem to = showItem $ sentWithTime_ ts tz [to <> plainContent content] meta showSndItem to = showItem $ sentWithTime_ ts tz [to <> plainContent content] meta
showRcvItem from = showItem $ receivedWithTime_ ts tz from [] meta [plainContent content] False showRcvItem from = showItem $ receivedWithTime_ ts tz from [] meta [plainContent content] False
showSndItemProhibited to = showItem $ sentWithTime_ ts tz [to <> plainContent content <> " " <> prohibited] meta showSndItemProhibited to = showItem $ sentWithTime_ ts tz [to <> plainContent content <> " " <> prohibited] meta
@ -617,11 +625,12 @@ viewChatItem chat ci@ChatItem {chatDir, meta = meta@CIMeta {forwardedByMember},
prohibited = styled (colored Red) ("[unexpected chat item created, please report to developers]" :: String) prohibited = styled (colored Red) ("[unexpected chat item created, please report to developers]" :: String)
viewChatItemInfo :: AChatItem -> ChatItemInfo -> TimeZone -> [StyledString] viewChatItemInfo :: AChatItem -> ChatItemInfo -> TimeZone -> [StyledString]
viewChatItemInfo (AChatItem _ msgDir _ ChatItem {meta = CIMeta {itemTs, itemTimed, createdAt}}) ChatItemInfo {itemVersions} tz = viewChatItemInfo (AChatItem _ msgDir _ ChatItem {meta = CIMeta {itemTs, itemTimed, createdAt}}) ChatItemInfo {itemVersions, forwardedFromChatItem} tz =
["sent at: " <> ts itemTs] ["sent at: " <> ts itemTs]
<> receivedAt <> receivedAt
<> toBeDeletedAt <> toBeDeletedAt
<> versions <> versions
<> forwardedFrom'
where where
ts = styleTime . localTs tz ts = styleTime . localTs tz
receivedAt = case msgDir of receivedAt = case msgDir of
@ -634,7 +643,21 @@ viewChatItemInfo (AChatItem _ msgDir _ ChatItem {meta = CIMeta {itemTs, itemTime
if null itemVersions if null itemVersions
then [] then []
else ["message history:"] <> concatMap version itemVersions else ["message history:"] <> concatMap version itemVersions
version ChatItemVersion {msgContent, itemVersionTs} = prependFirst (ts itemVersionTs <> styleTime ": ") $ ttyMsgContent msgContent where
version ChatItemVersion {msgContent, itemVersionTs} = prependFirst (ts itemVersionTs <> styleTime ": ") $ ttyMsgContent msgContent
forwardedFrom' =
case forwardedFromChatItem of
Just fwdACI@(AChatItem _ fwdMsgDir fwdChatInfo _) ->
[plain $ "forwarded from: " <> maybe "" (<> ", ") fwdDir_ <> fwdItemId]
where
fwdDir_ = case (fwdMsgDir, fwdChatInfo) of
(SMDSnd, DirectChat ct) -> Just $ "you @" <> viewContactName ct
(SMDRcv, DirectChat ct) -> Just $ "@" <> viewContactName ct
(SMDSnd, GroupChat gInfo) -> Just $ "you #" <> viewGroupName gInfo
(SMDRcv, GroupChat gInfo) -> Just $ "#" <> viewGroupName gInfo
_ -> Nothing
fwdItemId = "chat item id: " <> (T.pack . show $ aChatItemId fwdACI)
_ -> []
localTs :: TimeZone -> UTCTime -> String localTs :: TimeZone -> UTCTime -> String
localTs tz ts = do localTs tz ts = do
@ -666,37 +689,45 @@ viewDeliveryReceipt = \case
MRBadMsgHash -> ttyError' "⩗!" MRBadMsgHash -> ttyError' "⩗!"
viewItemUpdate :: MsgDirectionI d => ChatInfo c -> ChatItem c d -> Bool -> CurrentTime -> TimeZone -> [StyledString] viewItemUpdate :: MsgDirectionI d => ChatInfo c -> ChatItem c d -> Bool -> CurrentTime -> TimeZone -> [StyledString]
viewItemUpdate chat ChatItem {chatDir, meta = meta@CIMeta {itemEdited, itemLive}, content, quotedItem} liveItems ts tz = case chat of viewItemUpdate chat ChatItem {chatDir, meta = meta@CIMeta {itemForwarded, itemEdited, itemLive}, content, quotedItem} liveItems ts tz = case chat of
DirectChat c -> case chatDir of DirectChat c -> case chatDir of
CIDirectRcv -> case content of CIDirectRcv -> case content of
CIRcvMsgContent mc CIRcvMsgContent mc
| itemLive == Just True && not liveItems -> [] | itemLive == Just True && not liveItems -> []
| otherwise -> viewReceivedUpdatedMessage from quote mc ts tz meta | otherwise -> viewReceivedUpdatedMessage from context mc ts tz meta
_ -> [] _ -> []
where where
from = if itemEdited then ttyFromContactEdited c else ttyFromContact c from = if itemEdited then ttyFromContactEdited c else ttyFromContact c
CIDirectSnd -> case content of CIDirectSnd -> case content of
CISndMsgContent mc -> hideLive meta $ viewSentMessage to quote mc ts tz meta CISndMsgContent mc -> hideLive meta $ viewSentMessage to context mc ts tz meta
_ -> [] _ -> []
where where
to = if itemEdited then ttyToContactEdited' c else ttyToContact' c to = if itemEdited then ttyToContactEdited' c else ttyToContact' c
where where
quote = maybe [] (directQuote chatDir) quotedItem context =
maybe
(maybe [] forwardedFrom itemForwarded)
(directQuote chatDir)
quotedItem
GroupChat g -> case chatDir of GroupChat g -> case chatDir of
CIGroupRcv m -> case content of CIGroupRcv m -> case content of
CIRcvMsgContent mc CIRcvMsgContent mc
| itemLive == Just True && not liveItems -> [] | itemLive == Just True && not liveItems -> []
| otherwise -> viewReceivedUpdatedMessage from quote mc ts tz meta | otherwise -> viewReceivedUpdatedMessage from context mc ts tz meta
_ -> [] _ -> []
where where
from = if itemEdited then ttyFromGroupEdited g m else ttyFromGroup g m from = if itemEdited then ttyFromGroupEdited g m else ttyFromGroup g m
CIGroupSnd -> case content of CIGroupSnd -> case content of
CISndMsgContent mc -> hideLive meta $ viewSentMessage to quote mc ts tz meta CISndMsgContent mc -> hideLive meta $ viewSentMessage to context mc ts tz meta
_ -> [] _ -> []
where where
to = if itemEdited then ttyToGroupEdited g else ttyToGroup g to = if itemEdited then ttyToGroupEdited g else ttyToGroup g
where where
quote = maybe [] (groupQuote g) quotedItem context =
maybe
(maybe [] forwardedFrom itemForwarded)
(groupQuote g)
quotedItem
_ -> [] _ -> []
hideLive :: CIMeta c d -> [StyledString] -> [StyledString] hideLive :: CIMeta c d -> [StyledString] -> [StyledString]
@ -778,6 +809,14 @@ directQuote _ CIQuote {content = qmc, chatDir = quoteDir} =
groupQuote :: GroupInfo -> CIQuote 'CTGroup -> [StyledString] groupQuote :: GroupInfo -> CIQuote 'CTGroup -> [StyledString]
groupQuote g CIQuote {content = qmc, chatDir = quoteDir} = quoteText qmc . ttyQuotedMember $ sentByMember g quoteDir groupQuote g CIQuote {content = qmc, chatDir = quoteDir} = quoteText qmc . ttyQuotedMember $ sentByMember g quoteDir
forwardedFrom :: CIForwardedFrom -> [StyledString]
forwardedFrom = \case
CIFFUnknown -> ["-> forwarded"]
CIFFContact c MDSnd _ _ -> ["<- you @" <> (plain . viewName) c]
CIFFContact c MDRcv _ _ -> ["<- @" <> (plain . viewName) c]
CIFFGroup g MDSnd _ _ -> ["<- you #" <> (plain . viewName) g]
CIFFGroup g MDRcv _ _ -> ["<- #" <> (plain . viewName) g]
sentByMember :: GroupInfo -> CIQDirection 'CTGroup -> Maybe GroupMember sentByMember :: GroupInfo -> CIQDirection 'CTGroup -> Maybe GroupMember
sentByMember GroupInfo {membership} = \case sentByMember GroupInfo {membership} = \case
CIQGroupSnd -> Just membership CIQGroupSnd -> Just membership
@ -836,7 +875,9 @@ viewChatCleared :: AChatInfo -> [StyledString]
viewChatCleared (AChatInfo _ chatInfo) = case chatInfo of viewChatCleared (AChatInfo _ chatInfo) = case chatInfo of
DirectChat ct -> [ttyContact' ct <> ": all messages are removed locally ONLY"] DirectChat ct -> [ttyContact' ct <> ": all messages are removed locally ONLY"]
GroupChat gi -> [ttyGroup' gi <> ": all messages are removed locally ONLY"] GroupChat gi -> [ttyGroup' gi <> ": all messages are removed locally ONLY"]
_ -> [] LocalChat _ -> ["notes: all messages are removed"]
ContactRequest _ -> []
ContactConnection _ -> []
viewContactsList :: [Contact] -> [StyledString] viewContactsList :: [Contact] -> [StyledString]
viewContactsList = viewContactsList =
@ -1484,17 +1525,17 @@ viewReceivedUpdatedMessage :: StyledString -> [StyledString] -> MsgContent -> Cu
viewReceivedUpdatedMessage = viewReceivedMessage_ True viewReceivedUpdatedMessage = viewReceivedMessage_ True
viewReceivedMessage_ :: Bool -> StyledString -> [StyledString] -> MsgContent -> CurrentTime -> TimeZone -> CIMeta c d -> [StyledString] viewReceivedMessage_ :: Bool -> StyledString -> [StyledString] -> MsgContent -> CurrentTime -> TimeZone -> CIMeta c d -> [StyledString]
viewReceivedMessage_ updated from quote mc ts tz meta = receivedWithTime_ ts tz from quote meta (ttyMsgContent mc) updated viewReceivedMessage_ updated from context mc ts tz meta = receivedWithTime_ ts tz from context meta (ttyMsgContent mc) updated
viewReceivedReaction :: StyledString -> [StyledString] -> StyledString -> CurrentTime -> TimeZone -> UTCTime -> [StyledString] viewReceivedReaction :: StyledString -> [StyledString] -> StyledString -> CurrentTime -> TimeZone -> UTCTime -> [StyledString]
viewReceivedReaction from styledMsg reactionText ts tz reactionTs = viewReceivedReaction from styledMsg reactionText ts tz reactionTs =
prependFirst (ttyMsgTime ts tz reactionTs <> " " <> from) (styledMsg <> [" " <> reactionText]) prependFirst (ttyMsgTime ts tz reactionTs <> " " <> from) (styledMsg <> [" " <> reactionText])
receivedWithTime_ :: CurrentTime -> TimeZone -> StyledString -> [StyledString] -> CIMeta c d -> [StyledString] -> Bool -> [StyledString] receivedWithTime_ :: CurrentTime -> TimeZone -> StyledString -> [StyledString] -> CIMeta c d -> [StyledString] -> Bool -> [StyledString]
receivedWithTime_ ts tz from quote CIMeta {itemId, itemTs, itemEdited, itemDeleted, itemLive} styledMsg updated = do receivedWithTime_ ts tz from context CIMeta {itemId, itemTs, itemEdited, itemDeleted, itemLive} styledMsg updated = do
prependFirst (ttyMsgTime ts tz itemTs <> " " <> from) (quote <> prependFirst (indent <> live) styledMsg) prependFirst (ttyMsgTime ts tz itemTs <> " " <> from) (context <> prependFirst (indent <> live) styledMsg)
where where
indent = if null quote then "" else " " indent = if null context then "" else " "
live live
| itemEdited || isJust itemDeleted = "" | itemEdited || isJust itemDeleted = ""
| otherwise = case itemLive of | otherwise = case itemLive of
@ -1522,9 +1563,9 @@ recent now tz time = do
|| (localNow < currentDay12 && localTime >= previousDay18 && localTimeDay < localNowDay) || (localNow < currentDay12 && localTime >= previousDay18 && localTimeDay < localNowDay)
viewSentMessage :: StyledString -> [StyledString] -> MsgContent -> CurrentTime -> TimeZone -> CIMeta c d -> [StyledString] viewSentMessage :: StyledString -> [StyledString] -> MsgContent -> CurrentTime -> TimeZone -> CIMeta c d -> [StyledString]
viewSentMessage to quote mc ts tz meta@CIMeta {itemEdited, itemDeleted, itemLive} = sentWithTime_ ts tz (prependFirst to $ quote <> prependFirst (indent <> live) (ttyMsgContent mc)) meta viewSentMessage to context mc ts tz meta@CIMeta {itemEdited, itemDeleted, itemLive} = sentWithTime_ ts tz (prependFirst to $ context <> prependFirst (indent <> live) (ttyMsgContent mc)) meta
where where
indent = if null quote then "" else " " indent = if null context then "" else " "
live live
| itemEdited || isJust itemDeleted = "" | itemEdited || isJust itemDeleted = ""
| otherwise = case itemLive of | otherwise = case itemLive of
@ -1926,6 +1967,7 @@ viewChatError logLevel testView = \case
CEFallbackToSMPProhibited fileId -> ["recipient tried to accept file " <> sShow fileId <> " via old protocol, prohibited"] CEFallbackToSMPProhibited fileId -> ["recipient tried to accept file " <> sShow fileId <> " via old protocol, prohibited"]
CEInlineFileProhibited _ -> ["A small file sent without acceptance - you can enable receiving such files with -f option."] CEInlineFileProhibited _ -> ["A small file sent without acceptance - you can enable receiving such files with -f option."]
CEInvalidQuote -> ["cannot reply to this message"] CEInvalidQuote -> ["cannot reply to this message"]
CEInvalidForward -> ["cannot forward this message"]
CEInvalidChatItemUpdate -> ["cannot update this item"] CEInvalidChatItemUpdate -> ["cannot update this item"]
CEInvalidChatItemDelete -> ["cannot delete this item"] CEInvalidChatItemDelete -> ["cannot delete this item"]
CEHasCurrentCall -> ["call already in progress"] CEHasCurrentCall -> ["call already in progress"]

View file

@ -3,6 +3,7 @@ module ChatTests where
import ChatTests.ChatList import ChatTests.ChatList
import ChatTests.Direct import ChatTests.Direct
import ChatTests.Files import ChatTests.Files
import ChatTests.Forward
import ChatTests.Groups import ChatTests.Groups
import ChatTests.Local import ChatTests.Local
import ChatTests.Profiles import ChatTests.Profiles
@ -11,6 +12,7 @@ import Test.Hspec hiding (it)
chatTests :: SpecWith FilePath chatTests :: SpecWith FilePath
chatTests = do chatTests = do
describe "direct tests" chatDirectTests describe "direct tests" chatDirectTests
describe "forward tests" chatForwardTests
describe "group tests" chatGroupTests describe "group tests" chatGroupTests
describe "local chats tests" chatLocalChatsTests describe "local chats tests" chatLocalChatsTests
describe "file tests" chatFileTests describe "file tests" chatFileTests

View file

@ -617,20 +617,8 @@ testXFTPWithRelativePaths =
withXFTPServer $ do withXFTPServer $ do
-- agent is passed xftp work directory only on chat start, -- agent is passed xftp work directory only on chat start,
-- so for test we work around by stopping and starting chat -- so for test we work around by stopping and starting chat
alice ##> "/_stop" setRelativePaths alice "./tests/fixtures" "./tests/tmp/alice_xftp"
alice <## "chat stopped" setRelativePaths bob "./tests/tmp/bob_files" "./tests/tmp/bob_xftp"
alice #$> ("/_files_folder ./tests/fixtures", id, "ok")
alice #$> ("/_temp_folder ./tests/tmp/alice_xftp", id, "ok")
alice ##> "/_start"
alice <## "chat started"
bob ##> "/_stop"
bob <## "chat stopped"
bob #$> ("/_files_folder ./tests/tmp/bob_files", id, "ok")
bob #$> ("/_temp_folder ./tests/tmp/bob_xftp", id, "ok")
bob ##> "/_start"
bob <## "chat started"
connectUsers alice bob connectUsers alice bob
alice #> "/f @bob test.pdf" alice #> "/f @bob test.pdf"

523
tests/ChatTests/Forward.hs Normal file
View file

@ -0,0 +1,523 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PostfixOperators #-}
module ChatTests.Forward where
import ChatClient
import ChatTests.Utils
import qualified Data.ByteString.Char8 as B
import System.Directory (copyFile, doesFileExist)
import Test.Hspec hiding (it)
chatForwardTests :: SpecWith FilePath
chatForwardTests = do
describe "forward messages" $ do
it "from contact to contact" testForwardContactToContact
it "from contact to group" testForwardContactToGroup
it "from contact to notes" testForwardContactToNotes
it "from group to contact" testForwardGroupToContact
it "from group to group" testForwardGroupToGroup
it "from group to notes" testForwardGroupToNotes
it "from notes to contact" testForwardNotesToContact
it "from notes to group" testForwardNotesToGroup
it "from notes to notes" testForwardNotesToNotes -- TODO forward between different folders when supported
describe "interactions with forwarded messages" $ do
it "preserve original forward info" testForwardPreserveInfo
it "quoted message is not included" testForwardQuotedMsg
it "editing is prohibited" testForwardEditProhibited
describe "forward files" $ do
it "from contact to contact" testForwardFileNoFilesFolder
it "with relative paths: from contact to contact" testForwardFileContactToContact
it "with relative paths: from group to notes" testForwardFileGroupToNotes
it "with relative paths: from notes to group" testForwardFileNotesToGroup
testForwardContactToContact :: HasCallStack => FilePath -> IO ()
testForwardContactToContact =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
connectUsers alice bob
connectUsers alice cath
connectUsers bob cath
alice #> "@bob hi"
bob <# "alice> hi"
msgId <- lastItemId alice
bob #> "@alice hey"
alice <# "bob> hey"
alice ##> ("/_forward @3 @2 " <> msgId)
alice <# "@cath <- you @bob"
alice <## " hi"
cath <# "alice> -> forwarded"
cath <## " hi"
alice `send` "@cath <- @bob hey"
alice <# "@cath <- @bob"
alice <## " hey"
cath <# "alice> -> forwarded"
cath <## " hey"
-- read chat
alice ##> "/tail @cath 2"
alice <# "@cath <- you @bob"
alice <## " hi"
alice <# "@cath <- @bob"
alice <## " hey"
cath ##> "/tail @alice 2"
cath <# "alice> -> forwarded"
cath <## " hi"
cath <# "alice> -> forwarded"
cath <## " hey"
-- item info
alice ##> "/item info @cath hey"
alice <##. "sent at: "
alice <## "message history:"
alice .<## ": hey"
alice <##. "forwarded from: @bob, chat item id:"
testForwardContactToGroup :: HasCallStack => FilePath -> IO ()
testForwardContactToGroup =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
connectUsers alice bob
createGroup2 "team" alice cath
alice #> "@bob hi"
bob <# "alice> hi"
bob #> "@alice hey"
alice <# "bob> hey"
alice `send` "#team <- @bob hi"
alice <# "#team <- you @bob"
alice <## " hi"
cath <# "#team alice> -> forwarded"
cath <## " hi"
alice `send` "#team <- @bob hey"
alice <# "#team <- @bob"
alice <## " hey"
cath <# "#team alice> -> forwarded"
cath <## " hey"
testForwardContactToNotes :: HasCallStack => FilePath -> IO ()
testForwardContactToNotes =
testChat2 aliceProfile bobProfile $
\alice bob -> do
createCCNoteFolder alice
connectUsers alice bob
alice #> "@bob hi"
bob <# "alice> hi"
bob #> "@alice hey"
alice <# "bob> hey"
alice `send` "* <- @bob hi"
alice <# "* <- you @bob"
alice <## " hi"
alice `send` "* <- @bob hey"
alice <# "* <- @bob"
alice <## " hey"
testForwardGroupToContact :: HasCallStack => FilePath -> IO ()
testForwardGroupToContact =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup2 "team" alice bob
connectUsers alice cath
alice #> "#team hi"
bob <# "#team alice> hi"
bob #> "#team hey"
alice <# "#team bob> hey"
alice `send` "@cath <- #team hi"
alice <# "@cath <- you #team"
alice <## " hi"
cath <# "alice> -> forwarded"
cath <## " hi"
alice `send` "@cath <- #team @bob hey"
alice <# "@cath <- #team"
alice <## " hey"
cath <# "alice> -> forwarded"
cath <## " hey"
testForwardGroupToGroup :: HasCallStack => FilePath -> IO ()
testForwardGroupToGroup =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup2 "team" alice bob
createGroup2 "club" alice cath
alice #> "#team hi"
bob <# "#team alice> hi"
bob #> "#team hey"
alice <# "#team bob> hey"
alice `send` "#club <- #team hi"
alice <# "#club <- you #team"
alice <## " hi"
cath <# "#club alice> -> forwarded"
cath <## " hi"
alice `send` "#club <- #team hey"
alice <# "#club <- #team"
alice <## " hey"
cath <# "#club alice> -> forwarded"
cath <## " hey"
testForwardGroupToNotes :: HasCallStack => FilePath -> IO ()
testForwardGroupToNotes =
testChat2 aliceProfile bobProfile $
\alice bob -> do
createCCNoteFolder alice
createGroup2 "team" alice bob
alice #> "#team hi"
bob <# "#team alice> hi"
bob #> "#team hey"
alice <# "#team bob> hey"
alice `send` "* <- #team hi"
alice <# "* <- you #team"
alice <## " hi"
alice `send` "* <- #team hey"
alice <# "* <- #team"
alice <## " hey"
testForwardNotesToContact :: HasCallStack => FilePath -> IO ()
testForwardNotesToContact =
testChat2 aliceProfile cathProfile $
\alice cath -> do
createCCNoteFolder alice
connectUsers alice cath
alice /* "hi"
alice `send` "@cath <- * hi"
alice <# "@cath hi"
cath <# "alice> hi"
testForwardNotesToGroup :: HasCallStack => FilePath -> IO ()
testForwardNotesToGroup =
testChat2 aliceProfile cathProfile $
\alice cath -> do
createCCNoteFolder alice
createGroup2 "team" alice cath
alice /* "hi"
alice `send` "#team <- * hi"
alice <# "#team hi"
cath <# "#team alice> hi"
testForwardNotesToNotes :: HasCallStack => FilePath -> IO ()
testForwardNotesToNotes tmp =
withNewTestChat tmp "alice" aliceProfile $ \alice -> do
createCCNoteFolder alice
alice /* "hi"
alice `send` "* <- * hi"
alice <# "* hi"
alice ##> "/tail * 2"
alice <# "* hi"
alice <# "* hi"
testForwardPreserveInfo :: HasCallStack => FilePath -> IO ()
testForwardPreserveInfo =
testChat4 aliceProfile bobProfile cathProfile danProfile $
\alice bob cath dan -> do
createCCNoteFolder alice
connectUsers alice bob
connectUsers alice cath
createGroup2 "team" alice dan
bob #> "@alice hey"
alice <# "bob> hey"
alice `send` "* <- @bob hey"
alice <# "* <- @bob"
alice <## " hey"
alice `send` "@cath <- * hey"
alice <# "@cath <- @bob"
alice <## " hey"
cath <# "alice> -> forwarded"
cath <## " hey"
alice `send` "#team <- @cath hey"
alice <# "#team <- @bob"
alice <## " hey"
dan <# "#team alice> -> forwarded"
dan <## " hey"
testForwardQuotedMsg :: HasCallStack => FilePath -> IO ()
testForwardQuotedMsg =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
connectUsers alice bob
connectUsers alice cath
alice #> "@bob hi"
bob <# "alice> hi"
bob `send` "> @alice (hi) hey"
bob <# "@alice > hi"
bob <## " hey"
alice <# "bob> > hi"
alice <## " hey"
alice `send` "@cath <- @bob hey"
alice <# "@cath <- @bob"
alice <## " hey"
cath <# "alice> -> forwarded"
cath <## " hey"
-- read chat
alice ##> "/tail @cath 1"
alice <# "@cath <- @bob"
alice <## " hey"
cath ##> "/tail @alice 1"
cath <# "alice> -> forwarded"
cath <## " hey"
testForwardEditProhibited :: HasCallStack => FilePath -> IO ()
testForwardEditProhibited =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
connectUsers alice bob
connectUsers alice cath
bob #> "@alice hey"
alice <# "bob> hey"
alice `send` "@cath <- @bob hey"
alice <# "@cath <- @bob"
alice <## " hey"
cath <# "alice> -> forwarded"
cath <## " hey"
msgId <- lastItemId alice
alice ##> ("/_update item @3 " <> msgId <> " text hey edited")
alice <## "cannot update this item"
testForwardFileNoFilesFolder :: HasCallStack => FilePath -> IO ()
testForwardFileNoFilesFolder =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> withXFTPServer $ do
connectUsers alice bob
connectUsers bob cath
-- send original file
alice ##> "/_send @2 json {\"filePath\": \"./tests/fixtures/test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi\"}}"
alice <# "@bob hi"
alice <# "/f @bob ./tests/fixtures/test.pdf"
alice <## "use /fc 1 to cancel sending"
bob <# "alice> hi"
bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)"
bob <## "use /fr 1 [<dir>/ | <path>] to receive it"
bob ##> "/fr 1 ./tests/tmp"
concurrentlyN_
[ alice <## "completed uploading file 1 (test.pdf) for bob",
bob
<### [ "saving file 1 from alice to ./tests/tmp/test.pdf",
"started receiving file 1 (test.pdf) from alice"
]
]
bob <## "completed receiving file 1 (test.pdf) from alice"
src <- B.readFile "./tests/fixtures/test.pdf"
dest <- B.readFile "./tests/tmp/test.pdf"
dest `shouldBe` src
-- forward file
bob `send` "@cath <- @alice hi"
bob <# "@cath <- @alice"
bob <## " hi"
bob <# "/f @cath ./tests/tmp/test.pdf"
bob <## "use /fc 2 to cancel sending"
cath <# "bob> -> forwarded"
cath <## " hi"
cath <# "bob> sends file test.pdf (266.0 KiB / 272376 bytes)"
cath <## "use /fr 1 [<dir>/ | <path>] to receive it"
cath ##> "/fr 1 ./tests/tmp"
concurrentlyN_
[ bob <## "completed uploading file 2 (test.pdf) for cath",
cath
<### [ "saving file 1 from bob to ./tests/tmp/test_1.pdf",
"started receiving file 1 (test.pdf) from bob"
]
]
cath <## "completed receiving file 1 (test.pdf) from bob"
dest2 <- B.readFile "./tests/tmp/test_1.pdf"
dest2 `shouldBe` src
testForwardFileContactToContact :: HasCallStack => FilePath -> IO ()
testForwardFileContactToContact =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> withXFTPServer $ do
setRelativePaths alice "./tests/fixtures" "./tests/tmp/alice_xftp"
setRelativePaths bob "./tests/tmp/bob_files" "./tests/tmp/bob_xftp"
setRelativePaths cath "./tests/tmp/cath_files" "./tests/tmp/cath_xftp"
connectUsers alice bob
connectUsers bob cath
-- send original file
alice ##> "/_send @2 json {\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi\"}}"
alice <# "@bob hi"
alice <# "/f @bob test.pdf"
alice <## "use /fc 1 to cancel sending"
bob <# "alice> hi"
bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)"
bob <## "use /fr 1 [<dir>/ | <path>] to receive it"
bob ##> "/fr 1"
concurrentlyN_
[ alice <## "completed uploading file 1 (test.pdf) for bob",
bob
<### [ "saving file 1 from alice to test.pdf",
"started receiving file 1 (test.pdf) from alice"
]
]
bob <## "completed receiving file 1 (test.pdf) from alice"
src <- B.readFile "./tests/fixtures/test.pdf"
dest <- B.readFile "./tests/tmp/bob_files/test.pdf"
dest `shouldBe` src
-- forward file
bob `send` "@cath <- @alice hi"
bob <# "@cath <- @alice"
bob <## " hi"
bob <# "/f @cath test_1.pdf"
bob <## "use /fc 2 to cancel sending"
cath <# "bob> -> forwarded"
cath <## " hi"
cath <# "bob> sends file test_1.pdf (266.0 KiB / 272376 bytes)"
cath <## "use /fr 1 [<dir>/ | <path>] to receive it"
cath ##> "/fr 1"
concurrentlyN_
[ bob <## "completed uploading file 2 (test_1.pdf) for cath",
cath
<### [ "saving file 1 from bob to test_1.pdf",
"started receiving file 1 (test_1.pdf) from bob"
]
]
cath <## "completed receiving file 1 (test_1.pdf) from bob"
src2 <- B.readFile "./tests/tmp/bob_files/test_1.pdf"
src2 `shouldBe` dest
dest2 <- B.readFile "./tests/tmp/cath_files/test_1.pdf"
dest2 `shouldBe` src2
-- deleting original file doesn't delete forwarded file
checkActionDeletesFile "./tests/tmp/bob_files/test.pdf" $ do
bob ##> "/clear alice"
bob <## "alice: all messages are removed locally ONLY"
fwdFileExists <- doesFileExist "./tests/tmp/bob_files/test_1.pdf"
fwdFileExists `shouldBe` True
testForwardFileGroupToNotes :: HasCallStack => FilePath -> IO ()
testForwardFileGroupToNotes =
testChat2 aliceProfile cathProfile $
\alice cath -> withXFTPServer $ do
setRelativePaths alice "./tests/fixtures" "./tests/tmp/alice_xftp"
setRelativePaths cath "./tests/tmp/cath_files" "./tests/tmp/cath_xftp"
createGroup2 "team" alice cath
createCCNoteFolder cath
-- send original file
alice ##> "/_send #1 json {\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi\"}}"
alice <# "#team hi"
alice <# "/f #team test.pdf"
alice <## "use /fc 1 to cancel sending"
cath <# "#team alice> hi"
cath <# "#team alice> sends file test.pdf (266.0 KiB / 272376 bytes)"
cath <## "use /fr 1 [<dir>/ | <path>] to receive it"
cath ##> "/fr 1"
concurrentlyN_
[ alice <## "completed uploading file 1 (test.pdf) for #team",
cath
<### [ "saving file 1 from alice to test.pdf",
"started receiving file 1 (test.pdf) from alice"
]
]
cath <## "completed receiving file 1 (test.pdf) from alice"
src <- B.readFile "./tests/fixtures/test.pdf"
dest <- B.readFile "./tests/tmp/cath_files/test.pdf"
dest `shouldBe` src
-- forward file
cath `send` "* <- #team hi"
cath <# "* <- #team"
cath <## " hi"
cath <# "* file 2 (test_1.pdf)"
dest2 <- B.readFile "./tests/tmp/cath_files/test_1.pdf"
dest2 `shouldBe` dest
-- deleting original file doesn't delete forwarded file
checkActionDeletesFile "./tests/tmp/cath_files/test.pdf" $ do
cath ##> "/clear #team"
cath <## "#team: all messages are removed locally ONLY"
fwdFileExists <- doesFileExist "./tests/tmp/cath_files/test_1.pdf"
fwdFileExists `shouldBe` True
testForwardFileNotesToGroup :: HasCallStack => FilePath -> IO ()
testForwardFileNotesToGroup =
testChat2 aliceProfile cathProfile $
\alice cath -> withXFTPServer $ do
setRelativePaths alice "./tests/tmp/alice_files" "./tests/tmp/alice_xftp"
setRelativePaths cath "./tests/tmp/cath_files" "./tests/tmp/cath_xftp"
copyFile "./tests/fixtures/test.pdf" "./tests/tmp/alice_files/test.pdf"
createCCNoteFolder alice
createGroup2 "team" alice cath
-- create original file
alice ##> "/_create *1 json {\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi\"}}"
alice <# "* hi"
alice <# "* file 1 (test.pdf)"
-- forward file
alice `send` "#team <- * hi"
alice <# "#team hi"
alice <# "/f #team test_1.pdf"
alice <## "use /fc 2 to cancel sending"
cath <# "#team alice> hi"
cath <# "#team alice> sends file test_1.pdf (266.0 KiB / 272376 bytes)"
cath <## "use /fr 1 [<dir>/ | <path>] to receive it"
cath ##> "/fr 1"
concurrentlyN_
[ alice <## "completed uploading file 2 (test_1.pdf) for #team",
cath
<### [ "saving file 1 from alice to test_1.pdf",
"started receiving file 1 (test_1.pdf) from alice"
]
]
cath <## "completed receiving file 1 (test_1.pdf) from alice"
src <- B.readFile "./tests/tmp/alice_files/test.pdf"
src2 <- B.readFile "./tests/tmp/alice_files/test_1.pdf"
src2 `shouldBe` src
dest2 <- B.readFile "./tests/tmp/cath_files/test_1.pdf"
dest2 `shouldBe` src2
-- deleting original file doesn't delete forwarded file
checkActionDeletesFile "./tests/tmp/alice_files/test.pdf" $ do
alice ##> "/clear *"
alice <## "notes: all messages are removed"
fwdFileExists <- doesFileExist "./tests/tmp/alice_files/test_1.pdf"
fwdFileExists `shouldBe` True

View file

@ -150,6 +150,7 @@ testFiles tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do
doesFileExist stored `shouldReturn` True doesFileExist stored `shouldReturn` True
alice ##> "/clear *" alice ##> "/clear *"
alice <## "notes: all messages are removed"
alice ##> "/fs 1" alice ##> "/fs 1"
alice <## "file 1 not found" alice <## "file 1 not found"
alice ##> "/tail" alice ##> "/tail"
@ -180,6 +181,7 @@ testOtherFiles =
bob ##> "/tail *" bob ##> "/tail *"
bob <# "* test" bob <# "* test"
bob ##> "/clear *" bob ##> "/clear *"
bob <## "notes: all messages are removed"
bob ##> "/tail *" bob ##> "/tail *"
bob ##> "/fs 1" bob ##> "/fs 1"
bob <## "receiving file 1 (test.jpg) complete, path: test.jpg" bob <## "receiving file 1 (test.jpg) complete, path: test.jpg"

View file

@ -723,3 +723,12 @@ linkAnotherSchema link
xftpCLI :: [String] -> IO [String] xftpCLI :: [String] -> IO [String]
xftpCLI params = lines <$> capture_ (withArgs params xftpClientCLI) xftpCLI params = lines <$> capture_ (withArgs params xftpClientCLI)
setRelativePaths :: HasCallStack => TestCC -> String -> String -> IO ()
setRelativePaths cc filesFolder tempFolder = do
cc ##> "/_stop"
cc <## "chat stopped"
cc #$> ("/_files_folder " <> filesFolder, id, "ok")
cc #$> ("/_temp_folder " <> tempFolder, id, "ok")
cc ##> "/_start"
cc <## "chat started"