Compare commits

..

5 Commits

Author SHA1 Message Date
Avently 6e875d4632 android: disable compression and extraction of libs 2024-09-13 18:12:56 +07:00
Evgeny 4cfda91124 core: fix ForwardConfirmation encoding (#4872) 2024-09-13 09:35:11 +01:00
Evgeny Poberezkin 7aec147cec core: update simplexmq 6.0.4.0 2024-09-12 22:19:17 +01:00
Evgeny f6f2044675 core: plan forwarding chat items, api types (#4865)
* core: plan forwarding chat items, api types

* remove empty content, refactor get items

* another refactor

* plan

* test

* more tests

* text
2024-09-12 15:21:29 +01:00
Stanislav Dmitrenko 5f0b5c5a9f desktop: fix vlc dependency (2) (#4869) 2024-09-12 14:51:57 +01:00
13 changed files with 298 additions and 262 deletions
@@ -9,171 +9,6 @@
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChat
struct NewUserProfile: View {
@State private var profile = Profile(displayName: "", fullName: "")
// Modals
@State private var showChooseSource = false
@State private var showImagePicker = false
@State private var showFilePicker = false
@State private var showTakePhoto = false
@State private var chosenImage: UIImage? = nil
@State private var alert: UserProfileAlert?
var body: some View {
List {
Section {
Text("""
Your profile is stored on your device and shared only with your contacts.
SimpleX servers cannot see your profile.
"""
)
}
Section {
ProfileImage(imageStr: profile.image, size: 128)
.padding(8)
.overlay {
if profile.image != nil {
overlayButton("xmark", color: .red, alignment: .topTrailing) { profile.image = nil }
}
overlayButton("pencil", color: .accentColor, alignment: .bottomTrailing) {
showChooseSource = true
}
}
.frame(maxWidth: .infinity, alignment: .center)
if showFullName {
nameField("Full name", text: $profile.fullName)
}
nameField("Profile name", text: $profile.displayName)
Button("Save and notify contacts", action: saveProfile).disabled(!canSaveProfile)
}
}
// Lifecycle
.task {
if let user = ChatModel.shared.currentUser {
profile = fromLocalProfile(user.profile)
}
}
.onChange(of: chosenImage) { image in
if let image {
profile.image = resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500)
} else {
profile.image = nil
}
}
// Modals
.confirmationDialog("Profile image", isPresented: $showChooseSource, titleVisibility: .visible) {
Button("Take picture") {
showTakePhoto = true
}
Button("Choose from library") {
showImagePicker = true
}
Button("Choose file") {
showFilePicker = true
}
if UIPasteboard.general.hasImages {
Button("Paste image") {
chosenImage = UIPasteboard.general.image
}
}
}
.fullScreenCover(isPresented: $showTakePhoto) {
ZStack {
Color.black.edgesIgnoringSafeArea(.all)
CameraImagePicker(image: $chosenImage)
}
}
.sheet(isPresented: $showImagePicker) {
LibraryImagePicker(image: $chosenImage) { _ in
await MainActor.run {
showImagePicker = false
}
}
}
.fileImporter(isPresented: $showFilePicker, allowedContentTypes: [.image]) { url in
}
.alert(item: $alert) { a in userProfileAlert(a, $profile.displayName) }
}
@ViewBuilder
private func overlayButton(
_ systemName: String,
color: Color,
alignment: Alignment,
action: @escaping () -> Void
) -> some View {
Image(systemName: systemName)
.foregroundStyle(color)
.imageScale(.large)
.padding(8)
.background(.bar)
.clipShape(Circle())
.onTapGesture(perform: action)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: alignment)
}
@ViewBuilder
private func nameField(
_ title: LocalizedStringKey,
text: Binding<String>
) -> some View {
let isValid = validDisplayName(text.wrappedValue)
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(title).foregroundStyle(.secondary).font(.caption)
Spacer()
Image(systemName: "exclamationmark.circle")
.foregroundColor(.red)
.opacity(isValid ? 0 : 1)
.onTapGesture {
alert = .invalidNameError(validName: mkValidName(profile.displayName))
}
}
TextField(title, text: text)
.padding(.vertical, 4)
.padding(.horizontal, 4)
.overlay {
RoundedRectangle(cornerRadius: 6, style: .continuous)
.stroke(isValid ? Color(.tertiaryLabel) : Color.red)
}
}.listRowSeparator(.hidden)
}
// MARK: Computed
private func validNewProfileName(_ user: User) -> Bool {
profile.displayName == user.profile.displayName || validDisplayName(profile.displayName.trimmingCharacters(in: .whitespaces))
}
private var showFullName: Bool {
profile.fullName != "" &&
profile.fullName != profile.displayName
}
private var canSaveProfile: Bool {
profile.displayName.trimmingCharacters(in: .whitespaces) != "" &&
validDisplayName(profile.displayName.trimmingCharacters(in: .whitespaces))
}
private func saveProfile() {
Task {
do {
profile.displayName = profile.displayName.trimmingCharacters(in: .whitespaces)
if let (newProfile, _) = try await apiUpdateProfile(profile: profile) {
DispatchQueue.main.async {
ChatModel.shared.updateCurrentUser(newProfile)
profile = newProfile
}
} else {
alert = .duplicateUserError
}
} catch {
logger.error("UserProfile apiUpdateProfile error: \(responseError(error))")
}
}
}
}
struct UserProfile: View { struct UserProfile: View {
@EnvironmentObject var chatModel: ChatModel @EnvironmentObject var chatModel: ChatModel
@State private var profile = Profile(displayName: "", fullName: "") @State private var profile = Profile(displayName: "", fullName: "")
@@ -0,0 +1,48 @@
{
"originHash" : "e2611d1e91fd8071abc106776ba14ee2e395d2ad08a78e073381294abc10f115",
"pins" : [
{
"identity" : "codescanner",
"kind" : "remoteSourceControl",
"location" : "https://github.com/twostraws/CodeScanner",
"state" : {
"revision" : "34da57fb63b47add20de8a85da58191523ccce57",
"version" : "2.5.0"
}
},
{
"identity" : "lzstring-swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Ibrahimhass/lzstring-swift",
"state" : {
"revision" : "7f62f21de5b18582a950e1753b775cc614722407"
}
},
{
"identity" : "swiftygif",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kirualex/SwiftyGif",
"state" : {
"revision" : "5e8619335d394901379c9add5c4c1c2f420b3800"
}
},
{
"identity" : "webrtc",
"kind" : "remoteSourceControl",
"location" : "https://github.com/simplex-chat/WebRTC.git",
"state" : {
"revision" : "34bedc50f9c58dccf4967ea59c7e6a47d620803b"
}
},
{
"identity" : "yams",
"kind" : "remoteSourceControl",
"location" : "https://github.com/jpsim/Yams",
"state" : {
"revision" : "9234124cff5e22e178988c18d8b95a8ae8007f76",
"version" : "5.1.2"
}
}
],
"version" : 3
}
@@ -32,7 +32,6 @@ android {
} }
manifestPlaceholders["app_name"] = "@string/app_name" manifestPlaceholders["app_name"] = "@string/app_name"
manifestPlaceholders["provider_authorities"] = "chat.simplex.app.provider" manifestPlaceholders["provider_authorities"] = "chat.simplex.app.provider"
manifestPlaceholders["extract_native_libs"] = rootProject.extra["compression.level"] as Int != 0
} }
buildTypes { buildTypes {
@@ -75,7 +74,6 @@ android {
resources { resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}" excludes += "/META-INF/{AL2.0,LGPL2.1}"
} }
jniLibs.useLegacyPackaging = rootProject.extra["compression.level"] as Int != 0
} }
android.sourceSets["main"].assets.setSrcDirs(listOf("../common/src/commonMain/resources/assets")) android.sourceSets["main"].assets.setSrcDirs(listOf("../common/src/commonMain/resources/assets"))
val isRelease = gradle.startParameter.taskNames.find { it.lowercase().contains("release") } != null val isRelease = gradle.startParameter.taskNames.find { it.lowercase().contains("release") } != null
@@ -33,7 +33,6 @@
android:fullBackupOnly="false" android:fullBackupOnly="false"
android:icon="@mipmap/icon" android:icon="@mipmap/icon"
android:label="${app_name}" android:label="${app_name}"
android:extractNativeLibs="${extract_native_libs}"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.SimpleX"> android:theme="@style/Theme.SimpleX">
<!-- android:localeConfig="@xml/locales_config"--> <!-- android:localeConfig="@xml/locales_config"-->
+6 -1
View File
@@ -99,10 +99,15 @@ kotlin {
val desktopMain by getting { val desktopMain by getting {
dependencies { dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.8.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.8.0")
implementation("com.github.Dansoftowner:jSystemThemeDetector:3.8") implementation("com.github.Dansoftowner:jSystemThemeDetector:3.8") {
exclude("net.java.dev.jna")
}
// For jSystemThemeDetector only
implementation("net.java.dev.jna:jna-platform:5.14.0")
implementation("com.sshtools:two-slices:0.9.0-SNAPSHOT") implementation("com.sshtools:two-slices:0.9.0-SNAPSHOT")
implementation("org.slf4j:slf4j-simple:2.0.12") implementation("org.slf4j:slf4j-simple:2.0.12")
implementation("uk.co.caprica:vlcj:4.8.3") implementation("uk.co.caprica:vlcj:4.8.3")
implementation("net.java.dev.jna:jna:5.14.0")
implementation("com.github.NanoHttpd.nanohttpd:nanohttpd:efb2ebf85a") implementation("com.github.NanoHttpd.nanohttpd:nanohttpd:efb2ebf85a")
implementation("com.github.NanoHttpd.nanohttpd:nanohttpd-websocket:efb2ebf85a") implementation("com.github.NanoHttpd.nanohttpd:nanohttpd-websocket:efb2ebf85a")
implementation("com.squareup.okhttp3:okhttp:4.12.0") implementation("com.squareup.okhttp3:okhttp:4.12.0")
+1 -1
View File
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package source-repository-package
type: git type: git
location: https://github.com/simplex-chat/simplexmq.git location: https://github.com/simplex-chat/simplexmq.git
tag: 946e16339e16e026f51185ebfb48c3a0c5a5b2e1 tag: f5e666ae4f41351d5d5ac416cd6fb1d5fadc8ab7
source-repository-package source-repository-package
type: git type: git
+1
View File
@@ -32,6 +32,7 @@ for ORIG_NAME in "${ORIG_NAMES[@]}"; do
(cd apk && zip -r -q -"$level" ../"$ORIG_NAME" .) (cd apk && zip -r -q -"$level" ../"$ORIG_NAME" .)
# Shouldn't be compressed because of Android requirement # Shouldn't be compressed because of Android requirement
(cd apk && zip -r -q -0 ../"$ORIG_NAME" resources.arsc) (cd apk && zip -r -q -0 ../"$ORIG_NAME" resources.arsc)
(cd apk && zip -r -q -0 ../"$ORIG_NAME" lib/**/*.so)
if [ $case_insensitive -eq 1 ]; then if [ $case_insensitive -eq 1 ]; then
# For case-insensitive file systems # For case-insensitive file systems
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"https://github.com/simplex-chat/simplexmq.git"."946e16339e16e026f51185ebfb48c3a0c5a5b2e1" = "1jkx2f14h5krmy467zyifsc58dys89pkpn08cyf1q9v78in7nwfd"; "https://github.com/simplex-chat/simplexmq.git"."f5e666ae4f41351d5d5ac416cd6fb1d5fadc8ab7" = "1cq9apm9vp40v4ck0wcbis4463q3cjd9fbx5511hhh6lah6llifc";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
+120 -72
View File
@@ -843,9 +843,7 @@ processChatCommand' vr = \case
CTContactConnection -> pure $ chatCmdError (Just user) "not supported" CTContactConnection -> pure $ chatCmdError (Just user) "not supported"
APIDeleteChatItem (ChatRef cType chatId) itemIds mode -> withUser $ \user -> case cType of APIDeleteChatItem (ChatRef cType chatId) itemIds mode -> withUser $ \user -> case cType of
CTDirect -> withContactLock "deleteChatItem" chatId $ do CTDirect -> withContactLock "deleteChatItem" chatId $ do
ct <- withStore $ \db -> getContact db vr user chatId (ct, items) <- getCommandDirectChatItems user chatId itemIds
(errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getDirectCI db) (L.toList itemIds))
unless (null errs) $ toView $ CRChatErrors (Just user) errs
case mode of case mode of
CIDMInternal -> deleteDirectCIs user ct items True False CIDMInternal -> deleteDirectCIs user ct items True False
CIDMBroadcast -> do CIDMBroadcast -> do
@@ -858,13 +856,9 @@ processChatCommand' vr = \case
if featureAllowed SCFFullDelete forUser ct if featureAllowed SCFFullDelete forUser ct
then deleteDirectCIs user ct items True False then deleteDirectCIs user ct items True False
else markDirectCIsDeleted user ct items True =<< liftIO getCurrentTime else markDirectCIsDeleted user ct items True =<< liftIO getCurrentTime
where
getDirectCI :: DB.Connection -> ChatItemId -> IO (Either ChatError (CChatItem 'CTDirect))
getDirectCI db itemId = runExceptT . withExceptT ChatErrorStore $ getDirectChatItem db user chatId itemId
CTGroup -> withGroupLock "deleteChatItem" chatId $ do CTGroup -> withGroupLock "deleteChatItem" chatId $ do
Group gInfo ms <- withStore $ \db -> getGroup db vr user chatId (gInfo, items) <- getCommandGroupChatItems user chatId itemIds
(errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getGroupCI db) (L.toList itemIds)) ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo
unless (null errs) $ toView $ CRChatErrors (Just user) errs
case mode of case mode of
CIDMInternal -> deleteGroupCIs user gInfo items True False Nothing =<< liftIO getCurrentTime CIDMInternal -> deleteGroupCIs user gInfo items True False Nothing =<< liftIO getCurrentTime
CIDMBroadcast -> do CIDMBroadcast -> do
@@ -874,17 +868,9 @@ processChatCommand' vr = \case
events = L.nonEmpty $ map (`XMsgDel` Nothing) msgIds events = L.nonEmpty $ map (`XMsgDel` Nothing) msgIds
mapM_ (sendGroupMessages user gInfo ms) events mapM_ (sendGroupMessages user gInfo ms) events
delGroupChatItems user gInfo items Nothing delGroupChatItems user gInfo items Nothing
where
getGroupCI :: DB.Connection -> ChatItemId -> IO (Either ChatError (CChatItem 'CTGroup))
getGroupCI db itemId = runExceptT . withExceptT ChatErrorStore $ getGroupChatItem db user chatId itemId
CTLocal -> do CTLocal -> do
nf <- withStore $ \db -> getNoteFolder db user chatId (nf, items) <- getCommandLocalChatItems user chatId itemIds
(errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getLocalCI db) (L.toList itemIds))
unless (null errs) $ toView $ CRChatErrors (Just user) errs
deleteLocalCIs user nf items True False deleteLocalCIs user nf items True False
where
getLocalCI :: DB.Connection -> ChatItemId -> IO (Either ChatError (CChatItem 'CTLocal))
getLocalCI db itemId = runExceptT . withExceptT ChatErrorStore $ getLocalChatItem db user chatId itemId
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 where
@@ -902,9 +888,8 @@ processChatCommand' vr = \case
itemsMsgIds :: [CChatItem c] -> [SharedMsgId] itemsMsgIds :: [CChatItem c] -> [SharedMsgId]
itemsMsgIds = mapMaybe (\(CChatItem _ ChatItem {meta = CIMeta {itemSharedMsgId}}) -> itemSharedMsgId) itemsMsgIds = mapMaybe (\(CChatItem _ ChatItem {meta = CIMeta {itemSharedMsgId}}) -> itemSharedMsgId)
APIDeleteMemberChatItem gId itemIds -> withUser $ \user -> withGroupLock "deleteChatItem" gId $ do APIDeleteMemberChatItem gId itemIds -> withUser $ \user -> withGroupLock "deleteChatItem" gId $ do
Group gInfo@GroupInfo {membership} ms <- withStore $ \db -> getGroup db vr user gId (gInfo@GroupInfo {membership}, items) <- getCommandGroupChatItems user gId itemIds
(errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getGroupCI db user) (L.toList itemIds)) ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo
unless (null errs) $ toView $ CRChatErrors (Just user) errs
assertDeletable gInfo items assertDeletable gInfo items
assertUserGroupRole gInfo GRAdmin assertUserGroupRole gInfo GRAdmin
let msgMemIds = itemsMsgMemIds gInfo items let msgMemIds = itemsMsgMemIds gInfo items
@@ -912,8 +897,6 @@ processChatCommand' vr = \case
mapM_ (sendGroupMessages user gInfo ms) events mapM_ (sendGroupMessages user gInfo ms) events
delGroupChatItems user gInfo items (Just membership) delGroupChatItems user gInfo items (Just membership)
where where
getGroupCI :: DB.Connection -> User -> ChatItemId -> IO (Either ChatError (CChatItem 'CTGroup))
getGroupCI db user itemId = runExceptT . withExceptT ChatErrorStore $ getGroupChatItem db user gId itemId
assertDeletable :: GroupInfo -> [CChatItem 'CTGroup] -> CM () assertDeletable :: GroupInfo -> [CChatItem 'CTGroup] -> CM ()
assertDeletable GroupInfo {membership = GroupMember {memberRole = membershipMemRole}} items = assertDeletable GroupInfo {membership = GroupMember {memberRole = membershipMemRole}} items =
unless (all itemDeletable items) $ throwChatError CEInvalidChatItemDelete unless (all itemDeletable items) $ throwChatError CEInvalidChatItemDelete
@@ -980,6 +963,51 @@ 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")
APIPlanForwardChatItems (ChatRef fromCType fromChatId) itemIds -> withUser $ \user -> case fromCType of
CTDirect -> planForward user . snd =<< getCommandDirectChatItems user fromChatId itemIds
CTGroup -> planForward user . snd =<< getCommandGroupChatItems user fromChatId itemIds
CTLocal -> planForward user . snd =<< getCommandLocalChatItems user fromChatId itemIds
CTContactRequest -> pure $ chatCmdError (Just user) "not supported"
CTContactConnection -> pure $ chatCmdError (Just user) "not supported"
where
planForward :: User -> [CChatItem c] -> CM ChatResponse
planForward user items = do
(itemIds', forwardErrors) <- unzip <$> mapM planItemForward items
let forwardConfirmation = case catMaybes forwardErrors of
[] -> Nothing
errs -> Just $ case mainErr of
FFENotAccepted _ -> FCFilesNotAccepted fileIds
FFEInProgress -> FCFilesInProgress filesCount
FFEMissing -> FCFilesMissing filesCount
FFEFailed -> FCFilesFailed filesCount
where
mainErr = minimum errs
fileIds = catMaybes $ map (\case FFENotAccepted ftId -> Just ftId; _ -> Nothing) errs
filesCount = length $ filter (mainErr ==) errs
pure CRForwardPlan {user, itemsCount = length itemIds, chatItemIds = catMaybes itemIds', forwardConfirmation}
where
planItemForward :: CChatItem c -> CM (Maybe ChatItemId, Maybe ForwardFileError)
planItemForward (CChatItem _ ci) = forwardMsgContent ci >>= maybe (pure (Nothing, Nothing)) (forwardContentPlan ci)
forwardContentPlan :: ChatItem c d -> MsgContent -> CM (Maybe ChatItemId, Maybe ForwardFileError)
forwardContentPlan ChatItem {file, meta = CIMeta {itemId}} mc = case file of
Nothing -> pure (Just itemId, Nothing)
Just CIFile {fileId, fileStatus, fileSource} -> case ciFileForwardError fileId fileStatus of
Just err -> pure $ itemIdWithoutFile err
Nothing -> case fileSource of
Just CryptoFile {filePath} -> do
exists <- doesFileExist . maybe filePath (</> filePath) =<< chatReadVar filesFolder
pure $ if exists then (Just itemId, Nothing) else itemIdWithoutFile FFEMissing
Nothing -> pure $ itemIdWithoutFile FFEMissing
where
itemIdWithoutFile err = (if hasContent then Just itemId else Nothing, Just err)
hasContent = case mc of
MCText _ -> True
MCLink {} -> True
MCImage {} -> True
MCVideo {text} -> text /= ""
MCVoice {text} -> text /= ""
MCFile t -> t /= ""
MCUnknown {} -> True
APIForwardChatItems (ChatRef toCType toChatId) (ChatRef fromCType fromChatId) itemIds itemTTL -> withUser $ \user -> case toCType of APIForwardChatItems (ChatRef toCType toChatId) (ChatRef fromCType fromChatId) itemIds itemTTL -> withUser $ \user -> case toCType of
CTDirect -> do CTDirect -> do
cmrs <- prepareForward user cmrs <- prepareForward user
@@ -987,96 +1015,76 @@ processChatCommand' vr = \case
Just cmrs' -> Just cmrs' ->
withContactLock "forwardChatItem, to contact" toChatId $ withContactLock "forwardChatItem, to contact" toChatId $
sendContactContentMessages user toChatId False itemTTL cmrs' sendContactContentMessages user toChatId False itemTTL cmrs'
Nothing -> throwChatError $ CEInternalError "no chat items to forward" Nothing -> pure $ CRNewChatItems user []
CTGroup -> do CTGroup -> do
cmrs <- prepareForward user cmrs <- prepareForward user
case L.nonEmpty cmrs of case L.nonEmpty cmrs of
Just cmrs' -> Just cmrs' ->
withGroupLock "forwardChatItem, to group" toChatId $ withGroupLock "forwardChatItem, to group" toChatId $
sendGroupContentMessages user toChatId False itemTTL cmrs' sendGroupContentMessages user toChatId False itemTTL cmrs'
Nothing -> throwChatError $ CEInternalError "no chat items to forward" Nothing -> pure $ CRNewChatItems user []
CTLocal -> do CTLocal -> do
cmrs <- prepareForward user cmrs <- prepareForward user
case L.nonEmpty cmrs of case L.nonEmpty cmrs of
Just cmrs' -> Just cmrs' ->
createNoteFolderContentItems user toChatId cmrs' createNoteFolderContentItems user toChatId cmrs'
Nothing -> throwChatError $ CEInternalError "no chat items to forward" Nothing -> pure $ CRNewChatItems user []
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 where
prepareForward :: User -> CM [ComposeMessageReq] prepareForward :: User -> CM [ComposeMessageReq]
prepareForward user = case fromCType of prepareForward user = case fromCType of
CTDirect -> withContactLock "forwardChatItem, from contact" fromChatId $ do CTDirect -> withContactLock "forwardChatItem, from contact" fromChatId $ do
ct <- withFastStore $ \db -> getContact db vr user fromChatId (ct, items) <- getCommandDirectChatItems user fromChatId itemIds
(errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getDirectCI db) (L.toList itemIds)) catMaybes <$> mapM (\ci -> ciComposeMsgReq ct ci <$$> prepareMsgReq ci) items
unless (null errs) $ toView $ CRChatErrors (Just user) errs
mapM (ciComposeMsgReq ct) items
where where
getDirectCI :: DB.Connection -> ChatItemId -> IO (Either ChatError (CChatItem 'CTDirect)) ciComposeMsgReq :: Contact -> CChatItem 'CTDirect -> (MsgContent, Maybe CryptoFile) -> ComposeMessageReq
getDirectCI db itemId = runExceptT . withExceptT ChatErrorStore $ getDirectChatItem db user fromChatId itemId ciComposeMsgReq ct (CChatItem md ci) (mc', file) =
ciComposeMsgReq :: Contact -> CChatItem 'CTDirect -> CM ComposeMessageReq
ciComposeMsgReq ct (CChatItem _ ci) = do
(mc, mDir) <- forwardMC ci
file <- forwardCryptoFile ci
let itemId = chatItemId' ci let itemId = chatItemId' ci
ciff = forwardCIFF ci $ Just (CIFFContact (forwardName ct) mDir (Just fromChatId) (Just itemId)) ciff = forwardCIFF ci $ Just (CIFFContact (forwardName ct) (toMsgDirection md) (Just fromChatId) (Just itemId))
pure (ComposedMessage file Nothing mc, ciff) in (ComposedMessage file Nothing mc', ciff)
where where
forwardName :: Contact -> ContactName forwardName :: Contact -> ContactName
forwardName Contact {profile = LocalProfile {displayName, localAlias}} forwardName Contact {profile = LocalProfile {displayName, localAlias}}
| localAlias /= "" = localAlias | localAlias /= "" = localAlias
| otherwise = displayName | otherwise = displayName
CTGroup -> withGroupLock "forwardChatItem, from group" fromChatId $ do CTGroup -> withGroupLock "forwardChatItem, from group" fromChatId $ do
gInfo <- withFastStore $ \db -> getGroupInfo db vr user fromChatId (gInfo, items) <- getCommandGroupChatItems user fromChatId itemIds
(errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getGroupCI db) (L.toList itemIds)) catMaybes <$> mapM (\ci -> ciComposeMsgReq gInfo ci <$$> prepareMsgReq ci) items
unless (null errs) $ toView $ CRChatErrors (Just user) errs
mapM (ciComposeMsgReq gInfo) items
where where
getGroupCI :: DB.Connection -> ChatItemId -> IO (Either ChatError (CChatItem 'CTGroup)) ciComposeMsgReq :: GroupInfo -> CChatItem 'CTGroup -> (MsgContent, Maybe CryptoFile) -> ComposeMessageReq
getGroupCI db itemId = runExceptT . withExceptT ChatErrorStore $ getGroupChatItem db user fromChatId itemId ciComposeMsgReq gInfo (CChatItem md ci) (mc', file) = do
ciComposeMsgReq :: GroupInfo -> CChatItem 'CTGroup -> CM ComposeMessageReq
ciComposeMsgReq gInfo (CChatItem _ ci) = do
(mc, mDir) <- forwardMC ci
file <- forwardCryptoFile ci
let itemId = chatItemId' ci let itemId = chatItemId' ci
ciff = forwardCIFF ci $ Just (CIFFGroup (forwardName gInfo) mDir (Just fromChatId) (Just itemId)) ciff = forwardCIFF ci $ Just (CIFFGroup (forwardName gInfo) (toMsgDirection md) (Just fromChatId) (Just itemId))
pure (ComposedMessage file Nothing mc, ciff) in (ComposedMessage file Nothing mc', ciff)
where where
forwardName :: GroupInfo -> ContactName forwardName :: GroupInfo -> ContactName
forwardName GroupInfo {groupProfile = GroupProfile {displayName}} = displayName forwardName GroupInfo {groupProfile = GroupProfile {displayName}} = displayName
CTLocal -> do CTLocal -> do
(errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getLocalCI db) (L.toList itemIds)) (_, items) <- getCommandLocalChatItems user fromChatId itemIds
unless (null errs) $ toView $ CRChatErrors (Just user) errs catMaybes <$> mapM (\ci -> ciComposeMsgReq ci <$$> prepareMsgReq ci) items
mapM ciComposeMsgReq items
where where
getLocalCI :: DB.Connection -> ChatItemId -> IO (Either ChatError (CChatItem 'CTLocal)) ciComposeMsgReq :: CChatItem 'CTLocal -> (MsgContent, Maybe CryptoFile) -> ComposeMessageReq
getLocalCI db itemId = runExceptT . withExceptT ChatErrorStore $ getLocalChatItem db user fromChatId itemId ciComposeMsgReq (CChatItem _ ci) (mc', file) =
ciComposeMsgReq :: CChatItem 'CTLocal -> CM ComposeMessageReq
ciComposeMsgReq (CChatItem _ ci) = do
(mc, _) <- forwardMC ci
file <- forwardCryptoFile ci
let ciff = forwardCIFF ci Nothing let ciff = forwardCIFF ci Nothing
pure (ComposedMessage file Nothing mc, ciff) in (ComposedMessage file Nothing mc', ciff)
CTContactRequest -> throwChatError $ CECommandError "not supported" CTContactRequest -> throwChatError $ CECommandError "not supported"
CTContactConnection -> throwChatError $ CECommandError "not supported" CTContactConnection -> throwChatError $ CECommandError "not supported"
where where
forwardMC :: ChatItem c d -> CM (MsgContent, MsgDirection) prepareMsgReq :: CChatItem c -> CM (Maybe (MsgContent, Maybe CryptoFile))
forwardMC ChatItem {meta = CIMeta {itemDeleted = Just _}} = throwChatError CEInvalidForward prepareMsgReq (CChatItem _ ci) = forwardMsgContent ci $>>= forwardContent ci
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 c d -> Maybe CIForwardedFrom -> Maybe CIForwardedFrom
forwardCIFF ChatItem {meta = CIMeta {itemForwarded}} ciff = case itemForwarded of forwardCIFF ChatItem {meta = CIMeta {itemForwarded}} ciff = case itemForwarded of
Nothing -> ciff Nothing -> ciff
Just CIFFUnknown -> ciff Just CIFFUnknown -> ciff
Just prevCIFF -> Just prevCIFF Just prevCIFF -> Just prevCIFF
forwardCryptoFile :: ChatItem c d -> CM (Maybe CryptoFile) forwardContent :: ChatItem c d -> MsgContent -> CM (Maybe (MsgContent, Maybe CryptoFile))
forwardCryptoFile ChatItem {file = Nothing} = pure Nothing forwardContent ChatItem {file = Nothing} mc = pure $ Just (mc, Nothing)
forwardCryptoFile ChatItem {file = Just ciFile} = case ciFile of forwardContent ChatItem {file = Just ciFile} mc = case ciFile of
CIFile {fileName, fileSource = Just fromCF@CryptoFile {filePath}} -> CIFile {fileName, fileSource = Just fromCF@CryptoFile {filePath}} ->
chatReadVar filesFolder >>= \case chatReadVar filesFolder >>= \case
Nothing -> Nothing ->
ifM (doesFileExist filePath) (pure $ Just fromCF) (pure Nothing) ifM (doesFileExist filePath) (pure $ Just (mc, Just fromCF)) (pure contentWithoutFile)
Just filesFolder -> do Just filesFolder -> do
let fsFromPath = filesFolder </> filePath let fsFromPath = filesFolder </> filePath
ifM ifM
@@ -1089,10 +1097,17 @@ processChatCommand' vr = \case
let toCF = CryptoFile fsNewPath cfArgs let toCF = CryptoFile fsNewPath cfArgs
-- to keep forwarded file in case original is deleted -- to keep forwarded file in case original is deleted
liftIOEither $ runExceptT $ withExceptT (ChatError . CEInternalError . show) $ copyCryptoFile (fromCF {filePath = fsFromPath} :: CryptoFile) toCF liftIOEither $ runExceptT $ withExceptT (ChatError . CEInternalError . show) $ copyCryptoFile (fromCF {filePath = fsFromPath} :: CryptoFile) toCF
pure $ Just (toCF {filePath = takeFileName fsNewPath} :: CryptoFile) pure $ Just (mc, Just (toCF {filePath = takeFileName fsNewPath} :: CryptoFile))
) )
(pure Nothing) (pure contentWithoutFile)
_ -> pure Nothing _ -> pure contentWithoutFile
where
contentWithoutFile = case mc of
MCImage {} -> Just (mc, Nothing)
MCLink {} -> Just (mc, Nothing)
_ | contentText /= "" -> Just (MCText contentText, Nothing)
_ -> Nothing
contentText = msgContentText mc
copyCryptoFile :: CryptoFile -> CryptoFile -> ExceptT CF.FTCryptoError IO () copyCryptoFile :: CryptoFile -> CryptoFile -> ExceptT CF.FTCryptoError IO ()
copyCryptoFile fromCF@CryptoFile {filePath = fsFromPath, cryptoArgs = fromArgs} toCF@CryptoFile {cryptoArgs = toArgs} = do copyCryptoFile fromCF@CryptoFile {filePath = fsFromPath, cryptoArgs = fromArgs} toCF@CryptoFile {cryptoArgs = toArgs} = do
fromSizeFull <- getFileSize fsFromPath fromSizeFull <- getFileSize fsFromPath
@@ -3087,6 +3102,38 @@ processChatCommand' vr = \case
| (msg_, (ComposedMessage {msgContent}, itemForwarded), f, q) <- | (msg_, (ComposedMessage {msgContent}, itemForwarded), f, q) <-
zipWith4 (,,,) msgs_ (L.toList cmrs') (L.toList ciFiles_) (L.toList quotedItems_) zipWith4 (,,,) msgs_ (L.toList cmrs') (L.toList ciFiles_) (L.toList quotedItems_)
] ]
getCommandDirectChatItems :: User -> Int64 -> NonEmpty ChatItemId -> CM (Contact, [CChatItem 'CTDirect])
getCommandDirectChatItems user ctId itemIds = do
ct <- withFastStore $ \db -> getContact db vr user ctId
(errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getDirectCI db) (L.toList itemIds))
unless (null errs) $ toView $ CRChatErrors (Just user) errs
pure (ct, items)
where
getDirectCI :: DB.Connection -> ChatItemId -> IO (Either ChatError (CChatItem 'CTDirect))
getDirectCI db itemId = runExceptT . withExceptT ChatErrorStore $ getDirectChatItem db user ctId itemId
getCommandGroupChatItems :: User -> Int64 -> NonEmpty ChatItemId -> CM (GroupInfo, [CChatItem 'CTGroup])
getCommandGroupChatItems user gId itemIds = do
gInfo <- withFastStore $ \db -> getGroupInfo db vr user gId
(errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getGroupCI db) (L.toList itemIds))
unless (null errs) $ toView $ CRChatErrors (Just user) errs
pure (gInfo, items)
where
getGroupCI :: DB.Connection -> ChatItemId -> IO (Either ChatError (CChatItem 'CTGroup))
getGroupCI db itemId = runExceptT . withExceptT ChatErrorStore $ getGroupChatItem db user gId itemId
getCommandLocalChatItems :: User -> Int64 -> NonEmpty ChatItemId -> CM (NoteFolder, [CChatItem 'CTLocal])
getCommandLocalChatItems user nfId itemIds = do
nf <- withStore $ \db -> getNoteFolder db user nfId
(errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getLocalCI db) (L.toList itemIds))
unless (null errs) $ toView $ CRChatErrors (Just user) errs
pure (nf, items)
where
getLocalCI :: DB.Connection -> ChatItemId -> IO (Either ChatError (CChatItem 'CTLocal))
getLocalCI db itemId = runExceptT . withExceptT ChatErrorStore $ getLocalChatItem db user nfId itemId
forwardMsgContent :: ChatItem c d -> CM (Maybe MsgContent)
forwardMsgContent ChatItem {meta = CIMeta {itemDeleted = Just _}} = pure Nothing -- this can be deleted after selection
forwardMsgContent ChatItem {content = CISndMsgContent fmc} = pure $ Just fmc
forwardMsgContent ChatItem {content = CIRcvMsgContent fmc} = pure $ Just fmc
forwardMsgContent _ = throwChatError CEInvalidForward
createNoteFolderContentItems :: User -> NoteFolderId -> NonEmpty ComposeMessageReq -> CM ChatResponse createNoteFolderContentItems :: User -> NoteFolderId -> NonEmpty ComposeMessageReq -> CM ChatResponse
createNoteFolderContentItems user folderId cmrs = do createNoteFolderContentItems user folderId cmrs = do
assertNoQuotes assertNoQuotes
@@ -7888,6 +7935,7 @@ chatCommandP =
"/_delete item " *> (APIDeleteChatItem <$> chatRefP <*> _strP <* A.space <*> ciDeleteMode), "/_delete item " *> (APIDeleteChatItem <$> chatRefP <*> _strP <* A.space <*> ciDeleteMode),
"/_delete member item #" *> (APIDeleteMemberChatItem <$> A.decimal <*> _strP), "/_delete member item #" *> (APIDeleteMemberChatItem <$> A.decimal <*> _strP),
"/_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 plan " *> (APIPlanForwardChatItems <$> chatRefP <*> _strP),
"/_forward " *> (APIForwardChatItems <$> chatRefP <* A.space <*> chatRefP <*> _strP <*> sendMessageTTLP), "/_forward " *> (APIForwardChatItems <$> chatRefP <* A.space <*> chatRefP <*> _strP <*> sendMessageTTLP),
"/_read user " *> (APIUserRead <$> A.decimal), "/_read user " *> (APIUserRead <$> A.decimal),
"/read user" $> UserRead, "/read user" $> UserRead,
+11
View File
@@ -298,6 +298,7 @@ data ChatCommand
| APIDeleteChatItem ChatRef (NonEmpty ChatItemId) CIDeleteMode | APIDeleteChatItem ChatRef (NonEmpty ChatItemId) CIDeleteMode
| APIDeleteMemberChatItem GroupId (NonEmpty ChatItemId) | APIDeleteMemberChatItem GroupId (NonEmpty ChatItemId)
| APIChatItemReaction {chatRef :: ChatRef, chatItemId :: ChatItemId, add :: Bool, reaction :: MsgReaction} | APIChatItemReaction {chatRef :: ChatRef, chatItemId :: ChatItemId, add :: Bool, reaction :: MsgReaction}
| APIPlanForwardChatItems {fromChatRef :: ChatRef, chatItemIds :: NonEmpty ChatItemId}
| APIForwardChatItems {toChatRef :: ChatRef, fromChatRef :: ChatRef, chatItemIds :: NonEmpty ChatItemId, ttl :: Maybe Int} | APIForwardChatItems {toChatRef :: ChatRef, fromChatRef :: ChatRef, chatItemIds :: NonEmpty ChatItemId, ttl :: Maybe Int}
| APIUserRead UserId | APIUserRead UserId
| UserRead | UserRead
@@ -649,6 +650,7 @@ data ChatResponse
| CRContactRequestAlreadyAccepted {user :: User, contact :: Contact} | CRContactRequestAlreadyAccepted {user :: User, contact :: Contact}
| CRLeftMemberUser {user :: User, groupInfo :: GroupInfo} | CRLeftMemberUser {user :: User, groupInfo :: GroupInfo}
| CRGroupDeletedUser {user :: User, groupInfo :: GroupInfo} | CRGroupDeletedUser {user :: User, groupInfo :: GroupInfo}
| CRForwardPlan {user :: User, itemsCount :: Int, chatItemIds :: [ChatItemId], forwardConfirmation :: Maybe ForwardConfirmation}
| CRRcvFileDescrReady {user :: User, chatItem :: AChatItem, rcvFileTransfer :: RcvFileTransfer, rcvFileDescr :: RcvFileDescr} | CRRcvFileDescrReady {user :: User, chatItem :: AChatItem, rcvFileTransfer :: RcvFileTransfer, rcvFileDescr :: RcvFileDescr}
| CRRcvFileAccepted {user :: User, chatItem :: AChatItem} | CRRcvFileAccepted {user :: User, chatItem :: AChatItem}
| CRRcvFileAcceptedSndCancelled {user :: User, rcvFileTransfer :: RcvFileTransfer} | CRRcvFileAcceptedSndCancelled {user :: User, rcvFileTransfer :: RcvFileTransfer}
@@ -905,6 +907,13 @@ connectionPlanProceed = \case
GLPConnectingConfirmReconnect -> True GLPConnectingConfirmReconnect -> True
_ -> False _ -> False
data ForwardConfirmation
= FCFilesNotAccepted {fileIds :: [FileTransferId]}
| FCFilesInProgress {filesCount :: Int}
| FCFilesMissing {filesCount :: Int}
| FCFilesFailed {filesCount :: Int}
deriving (Show)
newtype UserPwd = UserPwd {unUserPwd :: Text} newtype UserPwd = UserPwd {unUserPwd :: Text}
deriving (Eq, Show) deriving (Eq, Show)
@@ -1463,6 +1472,8 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "GLP") ''GroupLinkPlan)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CP") ''ConnectionPlan) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CP") ''ConnectionPlan)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "FC") ''ForwardConfirmation)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CE") ''ChatErrorType) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CE") ''ChatErrorType)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "RHE") ''RemoteHostError) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "RHE") ''RemoteHostError)
+21
View File
@@ -595,6 +595,27 @@ ciFileLoaded = \case
CIFSRcvWarning {} -> False CIFSRcvWarning {} -> False
CIFSInvalid {} -> False CIFSInvalid {} -> False
data ForwardFileError = FFENotAccepted FileTransferId | FFEInProgress | FFEFailed | FFEMissing
deriving (Eq, Ord)
ciFileForwardError :: FileTransferId -> CIFileStatus d -> Maybe ForwardFileError
ciFileForwardError fId = \case
CIFSSndStored -> Nothing
CIFSSndTransfer {} -> Nothing
CIFSSndComplete -> Nothing
CIFSSndCancelled -> Nothing
CIFSSndError {} -> Nothing
CIFSSndWarning {} -> Nothing
CIFSRcvInvitation -> Just $ FFENotAccepted fId
CIFSRcvAccepted -> Just FFEInProgress
CIFSRcvTransfer {} -> Just FFEInProgress
CIFSRcvAborted -> Just $ FFENotAccepted fId
CIFSRcvCancelled -> Just FFEFailed
CIFSRcvComplete -> Nothing
CIFSRcvError {} -> Just FFEFailed
CIFSRcvWarning {} -> Just FFEFailed
CIFSInvalid {} -> Just FFEFailed
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
+16 -1
View File
@@ -203,6 +203,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
CRUnknownMemberBlocked u g byM um -> ttyUser u [ttyGroup' g <> ": " <> ttyMember byM <> " blocked an unknown member, creating unknown member record " <> ttyMember um] CRUnknownMemberBlocked u g byM um -> ttyUser u [ttyGroup' g <> ": " <> ttyMember byM <> " blocked an unknown member, creating unknown member record " <> ttyMember um]
CRUnknownMemberAnnounced u g _ um m -> ttyUser u [ttyGroup' g <> ": unknown member " <> ttyMember um <> " updated to " <> ttyMember m] CRUnknownMemberAnnounced u g _ um m -> ttyUser u [ttyGroup' g <> ": unknown member " <> ttyMember um <> " updated to " <> ttyMember m]
CRGroupDeletedUser u g -> ttyUser u [ttyGroup' g <> ": you deleted the group"] CRGroupDeletedUser u g -> ttyUser u [ttyGroup' g <> ": you deleted the group"]
CRForwardPlan u count itemIds fc -> ttyUser u $ viewForwardPlan count itemIds fc
CRRcvFileDescrReady _ _ _ _ -> [] CRRcvFileDescrReady _ _ _ _ -> []
CRRcvFileProgressXFTP {} -> [] CRRcvFileProgressXFTP {} -> []
CRRcvFileAccepted u ci -> ttyUser u $ savingFile' ci CRRcvFileAccepted u ci -> ttyUser u $ savingFile' ci
@@ -930,6 +931,20 @@ viewUserContactLinkDeleted =
"To create a new chat address use " <> highlight' "/ad" "To create a new chat address use " <> highlight' "/ad"
] ]
viewForwardPlan :: Int -> [ChatItemId] -> Maybe ForwardConfirmation -> [StyledString]
viewForwardPlan count itemIds = maybe [forwardCount] $ \fc -> [confirmation fc, forwardCount]
where
confirmation = \case
FCFilesNotAccepted fileIds -> plain $ "Files can be received: " <> intercalate ", " (map show fileIds)
FCFilesInProgress cnt -> plain $ "Still receiving " <> show cnt <> " file(s)"
FCFilesMissing cnt -> plain $ show cnt <> " file(s) are missing"
FCFilesFailed cnt -> plain $ "Receiving " <> show cnt <> " file(s) failed"
forwardCount
| count == len = "all messages can be forwarded"
| len == 0 = "nothing to forward"
| otherwise = plain $ show len <> " message(s) out of " <> show count <> " can be forwarded"
len = length itemIds
connReqContact_ :: StyledString -> ConnReqContact -> [StyledString] connReqContact_ :: StyledString -> ConnReqContact -> [StyledString]
connReqContact_ intro cReq = connReqContact_ intro cReq =
[ intro, [ intro,
@@ -2034,7 +2049,7 @@ viewChatError isCmd 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"] CEInvalidForward -> ["cannot forward message(s)"]
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"]
+73 -18
View File
@@ -7,7 +7,11 @@ import ChatClient
import ChatTests.Utils import ChatTests.Utils
import Control.Concurrent (threadDelay) import Control.Concurrent (threadDelay)
import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Char8 as B
import System.Directory (copyFile, doesFileExist) import Data.List (intercalate)
import qualified Data.Text as T
import System.Directory (copyFile, doesFileExist, removeFile)
import Simplex.Chat (fixedImagePreview)
import Simplex.Chat.Types (ImageData (..))
import Test.Hspec hiding (it) import Test.Hspec hiding (it)
chatForwardTests :: SpecWith FilePath chatForwardTests :: SpecWith FilePath
@@ -613,6 +617,8 @@ testForwardContactToContactMulti =
alice <# "bob> hey" alice <# "bob> hey"
msgId2 <- lastItemId alice msgId2 <- lastItemId alice
alice ##> ("/_forward plan @2 " <> msgId1 <> "," <> msgId2)
alice <## "all messages can be forwarded"
alice ##> ("/_forward @3 @2 " <> msgId1 <> "," <> msgId2) alice ##> ("/_forward @3 @2 " <> msgId1 <> "," <> msgId2)
alice <# "@cath <- you @bob" alice <# "@cath <- you @bob"
alice <## " hi" alice <## " hi"
@@ -642,6 +648,8 @@ testForwardGroupToGroupMulti =
alice <# "#team bob> hey" alice <# "#team bob> hey"
msgId2 <- lastItemId alice msgId2 <- lastItemId alice
alice ##> ("/_forward plan #1 " <> msgId1 <> "," <> msgId2)
alice <## "all messages can be forwarded"
alice ##> ("/_forward #2 #1 " <> msgId1 <> "," <> msgId2) alice ##> ("/_forward #2 #1 " <> msgId1 <> "," <> msgId2)
alice <# "#club <- you #team" alice <# "#club <- you #team"
alice <## " hi" alice <## " hi"
@@ -672,6 +680,7 @@ testMultiForwardFiles =
setRelativePaths alice "./tests/tmp/alice_app_files" "./tests/tmp/alice_xftp" setRelativePaths alice "./tests/tmp/alice_app_files" "./tests/tmp/alice_xftp"
copyFile "./tests/fixtures/test.jpg" "./tests/tmp/alice_app_files/test.jpg" copyFile "./tests/fixtures/test.jpg" "./tests/tmp/alice_app_files/test.jpg"
copyFile "./tests/fixtures/test.pdf" "./tests/tmp/alice_app_files/test.pdf" copyFile "./tests/fixtures/test.pdf" "./tests/tmp/alice_app_files/test.pdf"
copyFile "./tests/fixtures/test_1MB.pdf" "./tests/tmp/alice_app_files/test_1MB.pdf"
setRelativePaths bob "./tests/tmp/bob_app_files" "./tests/tmp/bob_xftp" setRelativePaths bob "./tests/tmp/bob_app_files" "./tests/tmp/bob_xftp"
setRelativePaths cath "./tests/tmp/cath_app_files" "./tests/tmp/cath_xftp" setRelativePaths cath "./tests/tmp/cath_app_files" "./tests/tmp/cath_xftp"
connectUsers alice bob connectUsers alice bob
@@ -686,32 +695,46 @@ testMultiForwardFiles =
-- send original files -- send original files
let cm1 = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message without file\"}}" let cm1 = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message without file\"}}"
cm2 = "{\"filePath\": \"test.jpg\", \"msgContent\": {\"type\": \"text\", \"text\": \"sending file 1\"}}" ImageData img = fixedImagePreview
cm3 = "{\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"sending file 2\"}}" cm2 = "{\"filePath\": \"test.jpg\", \"msgContent\": {\"type\": \"image\", \"image\":\"" <> T.unpack img <> "\", \"text\": \"\"}}"
alice ##> ("/_send @2 json [" <> cm1 <> "," <> cm2 <> "," <> cm3 <> "]") cm3 = "{\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"file\", \"text\": \"\"}}"
cm4 = "{\"filePath\": \"test_1MB.pdf\", \"msgContent\": {\"type\": \"file\", \"text\": \"message with large file\"}}"
alice ##> ("/_send @2 json [" <> cm1 <> "," <> cm2 <> "," <> cm3 <> "," <> cm4 <> "]")
alice <# "@bob message without file" alice <# "@bob message without file"
alice <# "@bob sending file 1"
alice <# "/f @bob test.jpg" alice <# "/f @bob test.jpg"
alice <## "use /fc 1 to cancel sending" alice <## "use /fc 1 to cancel sending"
alice <# "@bob sending file 2"
alice <# "/f @bob test.pdf" alice <# "/f @bob test.pdf"
alice <## "use /fc 2 to cancel sending" alice <## "use /fc 2 to cancel sending"
alice <# "@bob message with large file"
alice <# "/f @bob test_1MB.pdf"
alice <## "use /fc 3 to cancel sending"
bob <# "alice> message without file" bob <# "alice> message without file"
bob <# "alice> sending file 1"
bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)" bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)"
bob <## "use /fr 1 [<dir>/ | <path>] to receive it" bob <## "use /fr 1 [<dir>/ | <path>] to receive it"
bob <# "alice> sending file 2"
bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)" bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)"
bob <## "use /fr 2 [<dir>/ | <path>] to receive it" bob <## "use /fr 2 [<dir>/ | <path>] to receive it"
bob <# "alice> message with large file"
bob <# "alice> sends file test_1MB.pdf (1017.7 KiB / 1042157 bytes)"
bob <## "use /fr 3 [<dir>/ | <path>] to receive it"
alice <## "completed uploading file 1 (test.jpg) for bob" alice <## "completed uploading file 1 (test.jpg) for bob"
alice <## "completed uploading file 2 (test.pdf) for bob" alice <## "completed uploading file 2 (test.pdf) for bob"
alice <## "completed uploading file 3 (test_1MB.pdf) for bob"
-- IDs to forward
let msgId1 = (read msgIdZero :: Int) + 1
msgIds = intercalate "," $ map show [msgId1, msgId1 + 1, msgId1 + 2, msgId1 + 3, msgId1 + 4]
bob ##> ("/_forward plan @2 " <> msgIds)
bob <## "Files can be received: 1, 2, 3"
bob <## "4 message(s) out of 5 can be forwarded"
bob ##> "/fr 1" bob ##> "/fr 1"
bob bob
@@ -720,6 +743,10 @@ testMultiForwardFiles =
] ]
bob <## "completed receiving file 1 (test.jpg) from alice" bob <## "completed receiving file 1 (test.jpg) from alice"
bob ##> ("/_forward plan @2 " <> msgIds)
bob <## "Files can be received: 2, 3"
bob <## "4 message(s) out of 5 can be forwarded"
bob ##> "/fr 2" bob ##> "/fr 2"
bob bob
<### [ "saving file 2 from alice to test.pdf", <### [ "saving file 2 from alice to test.pdf",
@@ -736,8 +763,10 @@ testMultiForwardFiles =
dest2 `shouldBe` src2 dest2 `shouldBe` src2
-- forward file -- forward file
let msgId1 = (read msgIdZero :: Int) + 1 bob ##> ("/_forward plan @2 " <> msgIds)
bob ##> ("/_forward @3 @2 " <> show msgId1 <> "," <> show (msgId1 + 1) <> "," <> show (msgId1 + 2) <> "," <> show (msgId1 + 3)) bob <## "Files can be received: 3"
bob <## "all messages can be forwarded"
bob ##> ("/_forward @3 @2 " <> msgIds)
-- messages printed for bob -- messages printed for bob
bob <# "@cath <- you @alice" bob <# "@cath <- you @alice"
@@ -747,14 +776,17 @@ testMultiForwardFiles =
bob <## " message without file" bob <## " message without file"
bob <# "@cath <- @alice" bob <# "@cath <- @alice"
bob <## " sending file 1" bob <## " test_1.jpg"
bob <# "/f @cath test_1.jpg" bob <# "/f @cath test_1.jpg"
bob <## "use /fc 3 to cancel sending" bob <## "use /fc 4 to cancel sending"
bob <# "@cath <- @alice" bob <# "@cath <- @alice"
bob <## " sending file 2" bob <## " test_1.pdf"
bob <# "/f @cath test_1.pdf" bob <# "/f @cath test_1.pdf"
bob <## "use /fc 4 to cancel sending" bob <## "use /fc 5 to cancel sending"
bob <# "@cath <- @alice"
bob <## " message with large file"
-- messages printed for cath -- messages printed for cath
cath <# "bob> -> forwarded" cath <# "bob> -> forwarded"
@@ -764,18 +796,21 @@ testMultiForwardFiles =
cath <## " message without file" cath <## " message without file"
cath <# "bob> -> forwarded" cath <# "bob> -> forwarded"
cath <## " sending file 1" cath <## " test_1.jpg"
cath <# "bob> sends file test_1.jpg (136.5 KiB / 139737 bytes)" cath <# "bob> sends file test_1.jpg (136.5 KiB / 139737 bytes)"
cath <## "use /fr 1 [<dir>/ | <path>] to receive it" cath <## "use /fr 1 [<dir>/ | <path>] to receive it"
cath <# "bob> -> forwarded" cath <# "bob> -> forwarded"
cath <## " sending file 2" cath <## " test_1.pdf"
cath <# "bob> sends file test_1.pdf (266.0 KiB / 272376 bytes)" cath <# "bob> sends file test_1.pdf (266.0 KiB / 272376 bytes)"
cath <## "use /fr 2 [<dir>/ | <path>] to receive it" cath <## "use /fr 2 [<dir>/ | <path>] to receive it"
cath <# "bob> -> forwarded"
cath <## " message with large file"
-- file transfer -- file transfer
bob <## "completed uploading file 3 (test_1.jpg) for cath" bob <## "completed uploading file 4 (test_1.jpg) for cath"
bob <## "completed uploading file 4 (test_1.pdf) for cath" bob <## "completed uploading file 5 (test_1.pdf) for cath"
cath ##> "/fr 1" cath ##> "/fr 1"
cath cath
@@ -801,6 +836,26 @@ testMultiForwardFiles =
dest2C <- B.readFile "./tests/tmp/cath_app_files/test_1.pdf" dest2C <- B.readFile "./tests/tmp/cath_app_files/test_1.pdf"
dest2C `shouldBe` src2B dest2C `shouldBe` src2B
bob ##> "/fr 3"
bob
<### [ "saving file 3 from alice to test_1MB.pdf",
"started receiving file 3 (test_1MB.pdf) from alice"
]
bob <## "completed receiving file 3 (test_1MB.pdf) from alice"
bob ##> ("/_forward plan @2 " <> msgIds)
bob <## "all messages can be forwarded"
removeFile "./tests/tmp/bob_app_files/test_1MB.pdf"
bob ##> ("/_forward plan @2 " <> msgIds)
bob <## "1 file(s) are missing"
bob <## "all messages can be forwarded"
removeFile "./tests/tmp/bob_app_files/test.pdf"
bob ##> ("/_forward plan @2 " <> msgIds)
bob <## "2 file(s) are missing"
bob <## "4 message(s) out of 5 can be forwarded"
-- deleting original file doesn't delete forwarded file -- deleting original file doesn't delete forwarded file
checkActionDeletesFile "./tests/tmp/bob_app_files/test.jpg" $ do checkActionDeletesFile "./tests/tmp/bob_app_files/test.jpg" $ do
bob ##> "/clear alice" bob ##> "/clear alice"