Compare commits

..

6 Commits

Author SHA1 Message Date
shum 4e5ee981e5 Revert "ui: comment smp proxy ui (#4204)"
This reverts commit a0d6ae15ab.
2024-05-21 14:04:40 +00:00
Evgeny Poberezkin c59eb06b8a 5.8-beta.2: ios 218, android 210, desktop 47 2024-05-20 21:23:40 +01:00
Evgeny Poberezkin 423fc96638 core: 5.8.0.2 (simplexmq 5.8.0.2) 2024-05-20 18:38:46 +01:00
spaced4ndy ba203faad4 android: check known relays before file reception, support user approval of unknown relays (#4196) 2024-05-20 17:58:30 +04:00
spaced4ndy ec7b35adb9 ios: check known relays before file reception, support user approval of unknown relays (#4044)
* ios: check known relays before file reception, support user approval of unknown relays

* add privacyAskToApproveRelays to AppSettings

* less text

* ui improvements, fixes

* fix ntf

* shorter alert

* simplify settings

* rename

* icon

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-05-20 17:49:19 +04:00
spaced4ndy d2d450d1d7 core: check known relays before file reception, support user approval of unknown relays (#4043)
* core: check known relays before file reception, support user approval of unknown relays

* comment

* reset on not approved agent error

* add privacyAskToApproveRelays to AppSettings

* filter distinct servers

* update simplexmq

* remember user_approved_relays

* refactor

* rename

* update simplexmq

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-05-20 17:23:29 +04:00
49 changed files with 490 additions and 366 deletions
+45 -13
View File
@@ -927,14 +927,19 @@ func standaloneFileInfo(url: String, ctrl: chat_ctrl? = nil) async -> MigrationF
} }
} }
func receiveFile(user: any UserLike, fileId: Int64, auto: Bool = false) async { func receiveFile(user: any UserLike, fileId: Int64, userApprovedRelays: Bool = false, auto: Bool = false) async {
if let chatItem = await apiReceiveFile(fileId: fileId, encrypted: privacyEncryptLocalFilesGroupDefault.get(), auto: auto) { if let chatItem = await apiReceiveFile(
fileId: fileId,
userApprovedRelays: userApprovedRelays || !privacyAskToApproveRelaysGroupDefault.get(),
encrypted: privacyEncryptLocalFilesGroupDefault.get(),
auto: auto
) {
await chatItemSimpleUpdate(user, chatItem) await chatItemSimpleUpdate(user, chatItem)
} }
} }
func apiReceiveFile(fileId: Int64, encrypted: Bool, inline: Bool? = nil, auto: Bool = false) async -> AChatItem? { func apiReceiveFile(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool, inline: Bool? = nil, auto: Bool = false) async -> AChatItem? {
let r = await chatSendCmd(.receiveFile(fileId: fileId, encrypted: encrypted, inline: inline)) let r = await chatSendCmd(.receiveFile(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted, inline: inline))
let am = AlertManager.shared let am = AlertManager.shared
if case let .rcvFileAccepted(_, chatItem) = r { return chatItem } if case let .rcvFileAccepted(_, chatItem) = r { return chatItem }
if case .rcvFileAcceptedSndCancelled = r { if case .rcvFileAcceptedSndCancelled = r {
@@ -947,19 +952,50 @@ func apiReceiveFile(fileId: Int64, encrypted: Bool, inline: Bool? = nil, auto: B
} }
} else if let networkErrorAlert = networkErrorAlert(r) { } else if let networkErrorAlert = networkErrorAlert(r) {
logger.error("apiReceiveFile network error: \(String(describing: r))") logger.error("apiReceiveFile network error: \(String(describing: r))")
am.showAlert(networkErrorAlert) if !auto {
am.showAlert(networkErrorAlert)
}
} else { } else {
switch chatError(r) { switch chatError(r) {
case .fileCancelled: case .fileCancelled:
logger.debug("apiReceiveFile ignoring fileCancelled error") logger.debug("apiReceiveFile ignoring fileCancelled error")
case .fileAlreadyReceiving: case .fileAlreadyReceiving:
logger.debug("apiReceiveFile ignoring fileAlreadyReceiving error") logger.debug("apiReceiveFile ignoring fileAlreadyReceiving error")
case let .fileNotApproved(fileId, unknownServers):
logger.debug("apiReceiveFile fileNotApproved error")
if !auto {
let srvs = unknownServers.map { s in
if let srv = parseServerAddress(s), !srv.hostnames.isEmpty {
srv.hostnames[0]
} else {
serverHost(s)
}
}
am.showAlert(Alert(
title: Text("Unknown servers!"),
message: Text("Without Tor or VPN, your IP address will be visible to these XFTP relays: \(srvs.sorted().joined(separator: ", "))."),
primaryButton: .default(
Text("Download"),
action: {
Task {
logger.debug("apiReceiveFile fileNotApproved alert - in Task")
if let user = ChatModel.shared.currentUser {
await receiveFile(user: user, fileId: fileId, userApprovedRelays: true)
}
}
}
),
secondaryButton: .cancel()
))
}
default: default:
logger.error("apiReceiveFile error: \(String(describing: r))") logger.error("apiReceiveFile error: \(String(describing: r))")
am.showAlertMsg( if !auto {
title: "Error receiving file", am.showAlertMsg(
message: "Error: \(String(describing: r))" title: "Error receiving file",
) message: "Error: \(String(describing: r))"
)
}
} }
} }
return nil return nil
@@ -1286,10 +1322,6 @@ func apiGetVersion() throws -> CoreVersionInfo {
throw r throw r
} }
func apiSetAppLogLevel(_ ll: ChatLogLevel) async throws {
try await sendCommandOkResp(.setAppLogLevel(appLogLevel: ll))
}
private func currentUserId(_ funcName: String) throws -> Int64 { private func currentUserId(_ funcName: String) throws -> Int64 {
if let userId = ChatModel.shared.currentUser?.userId { if let userId = ChatModel.shared.currentUser?.userId {
return userId return userId
@@ -49,7 +49,7 @@ func localizedInfoRow(_ title: LocalizedStringKey, _ value: LocalizedStringKey)
} }
} }
private func serverHost(_ s: String) -> String { func serverHost(_ s: String) -> String {
if let i = s.range(of: "@")?.lowerBound { if let i = s.range(of: "@")?.lowerBound {
return String(s[i...].dropFirst()) return String(s[i...].dropFirst())
} else { } else {
@@ -60,6 +60,7 @@ struct CIFileView: View {
case .rcvInvitation: return true case .rcvInvitation: return true
case .rcvAccepted: return true case .rcvAccepted: return true
case .rcvTransfer: return false case .rcvTransfer: return false
case .rcvAborted: return true
case .rcvComplete: return true case .rcvComplete: return true
case .rcvCancelled: return false case .rcvCancelled: return false
case .rcvError: return false case .rcvError: return false
@@ -73,10 +74,10 @@ struct CIFileView: View {
logger.debug("CIFileView fileAction") logger.debug("CIFileView fileAction")
if let file = file { if let file = file {
switch (file.fileStatus) { switch (file.fileStatus) {
case .rcvInvitation: case .rcvInvitation, .rcvAborted:
if fileSizeValid(file) { if fileSizeValid(file) {
Task { Task {
logger.debug("CIFileView fileAction - in .rcvInvitation, in Task") logger.debug("CIFileView fileAction - in .rcvInvitation, .rcvAborted, in Task")
if let user = m.currentUser { if let user = m.currentUser {
await receiveFile(user: user, fileId: file.fileId) await receiveFile(user: user, fileId: file.fileId)
} }
@@ -148,6 +149,8 @@ struct CIFileView: View {
} else { } else {
progressView() progressView()
} }
case .rcvAborted:
fileIcon("doc.fill", color: .accentColor, innerIcon: "exclamationmark.arrow.circlepath", innerIconSize: 12)
case .rcvComplete: fileIcon("doc.fill") case .rcvComplete: fileIcon("doc.fill")
case .rcvCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10) case .rcvCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
case .rcvError: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10) case .rcvError: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
@@ -17,7 +17,6 @@ struct CIImageView: View {
let maxWidth: CGFloat let maxWidth: CGFloat
@Binding var imgWidth: CGFloat? @Binding var imgWidth: CGFloat?
@State var scrollProxy: ScrollViewProxy? @State var scrollProxy: ScrollViewProxy?
@State var metaColor: Color
@State private var showFullScreenImage = false @State private var showFullScreenImage = false
var body: some View { var body: some View {
@@ -38,7 +37,7 @@ struct CIImageView: View {
.onTapGesture { .onTapGesture {
if let file = file { if let file = file {
switch file.fileStatus { switch file.fileStatus {
case .rcvInvitation: case .rcvInvitation, .rcvAborted:
Task { Task {
if let user = m.currentUser { if let user = m.currentUser {
await receiveFile(user: user, fileId: file.fileId) await receiveFile(user: user, fileId: file.fileId)
@@ -103,6 +102,7 @@ struct CIImageView: View {
case .rcvInvitation: fileIcon("arrow.down", 10, 13) case .rcvInvitation: fileIcon("arrow.down", 10, 13)
case .rcvAccepted: fileIcon("ellipsis", 14, 11) case .rcvAccepted: fileIcon("ellipsis", 14, 11)
case .rcvTransfer: progressView() case .rcvTransfer: progressView()
case .rcvAborted: fileIcon("exclamationmark.arrow.circlepath", 14, 11)
case .rcvCancelled: fileIcon("xmark", 10, 13) case .rcvCancelled: fileIcon("xmark", 10, 13)
case .rcvError: fileIcon("xmark", 10, 13) case .rcvError: fileIcon("xmark", 10, 13)
case .invalid: fileIcon("questionmark", 10, 13) case .invalid: fileIcon("questionmark", 10, 13)
@@ -116,7 +116,7 @@ struct CIImageView: View {
.resizable() .resizable()
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: size, height: size) .frame(width: size, height: size)
.foregroundColor(metaColor) .foregroundColor(.white)
.padding(padding) .padding(padding)
} }
@@ -69,7 +69,7 @@ struct CIVideoView: View {
.onTapGesture { .onTapGesture {
if let file = file { if let file = file {
switch file.fileStatus { switch file.fileStatus {
case .rcvInvitation: case .rcvInvitation, .rcvAborted:
receiveFileIfValidSize(file: file, receiveFile: receiveFile) receiveFileIfValidSize(file: file, receiveFile: receiveFile)
case .rcvAccepted: case .rcvAccepted:
switch file.fileProtocol { switch file.fileProtocol {
@@ -95,7 +95,7 @@ struct CIVideoView: View {
} }
durationProgress() durationProgress()
} }
if let file = file, case .rcvInvitation = file.fileStatus { if let file = file, showDownloadButton(file.fileStatus) {
Button { Button {
receiveFileIfValidSize(file: file, receiveFile: receiveFile) receiveFileIfValidSize(file: file, receiveFile: receiveFile)
} label: { } label: {
@@ -105,6 +105,14 @@ struct CIVideoView: View {
} }
} }
private func showDownloadButton(_ fileStatus: CIFileStatus) -> Bool {
switch fileStatus {
case .rcvInvitation: true
case .rcvAborted: true
default: false
}
}
private func videoViewEncrypted(_ file: CIFile, _ defaultPreview: UIImage, _ duration: Int) -> some View { private func videoViewEncrypted(_ file: CIFile, _ defaultPreview: UIImage, _ duration: Int) -> some View {
return ZStack(alignment: .topTrailing) { return ZStack(alignment: .topTrailing) {
ZStack(alignment: .center) { ZStack(alignment: .center) {
@@ -280,6 +288,7 @@ struct CIVideoView: View {
} else { } else {
progressView() progressView()
} }
case .rcvAborted: fileIcon("exclamationmark.arrow.circlepath", 14, 11)
case .rcvCancelled: fileIcon("xmark", 10, 13) case .rcvCancelled: fileIcon("xmark", 10, 13)
case .rcvError: fileIcon("xmark", 10, 13) case .rcvError: fileIcon("xmark", 10, 13)
case .invalid: fileIcon("questionmark", 10, 13) case .invalid: fileIcon("questionmark", 10, 13)
@@ -318,10 +327,10 @@ struct CIVideoView: View {
} }
// TODO encrypt: where file size is checked? // TODO encrypt: where file size is checked?
private func receiveFileIfValidSize(file: CIFile, receiveFile: @escaping (User, Int64, Bool) async -> Void) { private func receiveFileIfValidSize(file: CIFile, receiveFile: @escaping (User, Int64, Bool, Bool) async -> Void) {
Task { Task {
if let user = m.currentUser { if let user = m.currentUser {
await receiveFile(user, file.fileId, false) await receiveFile(user, file.fileId, false, false)
} }
} }
} }
@@ -139,9 +139,10 @@ struct VoiceMessagePlayer: View {
case .sndComplete: playbackButton() case .sndComplete: playbackButton()
case .sndCancelled: playbackButton() case .sndCancelled: playbackButton()
case .sndError: playbackButton() case .sndError: playbackButton()
case .rcvInvitation: downloadButton(recordingFile) case .rcvInvitation: downloadButton(recordingFile, "play.fill")
case .rcvAccepted: loadingIcon() case .rcvAccepted: loadingIcon()
case .rcvTransfer: loadingIcon() case .rcvTransfer: loadingIcon()
case .rcvAborted: downloadButton(recordingFile, "exclamationmark.arrow.circlepath")
case .rcvComplete: playbackButton() case .rcvComplete: playbackButton()
case .rcvCancelled: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel)) case .rcvCancelled: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel))
case .rcvError: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel)) case .rcvError: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel))
@@ -217,7 +218,7 @@ struct VoiceMessagePlayer: View {
} }
} }
private func downloadButton(_ recordingFile: CIFile) -> some View { private func downloadButton(_ recordingFile: CIFile, _ icon: String) -> some View {
Button { Button {
Task { Task {
if let user = chatModel.currentUser { if let user = chatModel.currentUser {
@@ -225,7 +226,7 @@ struct VoiceMessagePlayer: View {
} }
} }
} label: { } label: {
playPauseIcon("play.fill") playPauseIcon(icon)
} }
} }
@@ -115,7 +115,7 @@ struct FramedItemView: View {
} else { } else {
switch (chatItem.content.msgContent) { switch (chatItem.content.msgContent) {
case let .image(text, image): case let .image(text, image):
CIImageView(chatItem: chatItem, image: image, maxWidth: maxWidth, imgWidth: $imgWidth, scrollProxy: scrollProxy, metaColor: metaColor) CIImageView(chatItem: chatItem, image: image, maxWidth: maxWidth, imgWidth: $imgWidth, scrollProxy: scrollProxy)
.overlay(DetermineWidth()) .overlay(DetermineWidth())
if text == "" && !chatItem.meta.isLive { if text == "" && !chatItem.meta.isLive {
Color.clear Color.clear
@@ -23,6 +23,7 @@ extension AppSettings {
setNetCfg(val) setNetCfg(val)
} }
if let val = privacyEncryptLocalFiles { privacyEncryptLocalFilesGroupDefault.set(val) } if let val = privacyEncryptLocalFiles { privacyEncryptLocalFilesGroupDefault.set(val) }
if let val = privacyAskToApproveRelays { privacyAskToApproveRelaysGroupDefault.set(val) }
if let val = privacyAcceptImages { if let val = privacyAcceptImages {
privacyAcceptImagesGroupDefault.set(val) privacyAcceptImagesGroupDefault.set(val)
def.setValue(val, forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES) def.setValue(val, forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES)
@@ -50,6 +51,7 @@ extension AppSettings {
var c = AppSettings.defaults var c = AppSettings.defaults
c.networkConfig = getNetCfg() c.networkConfig = getNetCfg()
c.privacyEncryptLocalFiles = privacyEncryptLocalFilesGroupDefault.get() c.privacyEncryptLocalFiles = privacyEncryptLocalFilesGroupDefault.get()
c.privacyAskToApproveRelays = privacyAskToApproveRelaysGroupDefault.get()
c.privacyAcceptImages = privacyAcceptImagesGroupDefault.get() c.privacyAcceptImages = privacyAcceptImagesGroupDefault.get()
c.privacyLinkPreviews = def.bool(forKey: DEFAULT_PRIVACY_LINK_PREVIEWS) c.privacyLinkPreviews = def.bool(forKey: DEFAULT_PRIVACY_LINK_PREVIEWS)
c.privacyShowChatPreviews = def.bool(forKey: DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) c.privacyShowChatPreviews = def.bool(forKey: DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS)
@@ -12,7 +12,6 @@ import SimpleXChat
struct DeveloperView: View { struct DeveloperView: View {
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
@AppStorage(GROUP_DEFAULT_CONFIRM_DB_UPGRADES, store: groupDefaults) private var confirmDatabaseUpgrades = false @AppStorage(GROUP_DEFAULT_CONFIRM_DB_UPGRADES, store: groupDefaults) private var confirmDatabaseUpgrades = false
@State private var appLogLevel = appLogLevelGroupDefault.get()
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
var body: some View { var body: some View {
@@ -38,24 +37,6 @@ struct DeveloperView: View {
settingsRow("chevron.left.forwardslash.chevron.right") { settingsRow("chevron.left.forwardslash.chevron.right") {
Toggle("Show developer options", isOn: $developerTools) Toggle("Show developer options", isOn: $developerTools)
} }
settingsRow("text.justify") {
Picker("Log level", selection: $appLogLevel) {
ForEach(ChatLogLevel.allCases, id: \.self) { ll in
Text(ll.rawValue)
}
}
.frame(height: 36)
.onChange(of: appLogLevel) { ll in
Task {
do {
try await apiSetAppLogLevel(ll)
appLogLevelGroupDefault.set(ll)
} catch let e {
logger.error("apiSetAppLogLevel error: \(responseError(e))")
}
}
}
}
} header: { } header: {
Text("") Text("")
} footer: { } footer: {
@@ -82,29 +82,29 @@ struct NetworkAndServers: View {
Text("Using .onion hosts requires compatible VPN provider.") Text("Using .onion hosts requires compatible VPN provider.")
} }
// Section { Section {
// Picker("Private routing", selection: $proxyMode) { Picker("Private routing", selection: $proxyMode) {
// ForEach(SMPProxyMode.values, id: \.self) { Text($0.text) } ForEach(SMPProxyMode.values, id: \.self) { Text($0.text) }
// } }
// .frame(height: 36) .frame(height: 36)
//
// Picker("Allow downgrade", selection: $proxyFallback) { Picker("Allow downgrade", selection: $proxyFallback) {
// ForEach(SMPProxyFallback.values, id: \.self) { Text($0.text) } ForEach(SMPProxyFallback.values, id: \.self) { Text($0.text) }
// } }
// .disabled(proxyMode == .never) .disabled(proxyMode == .never)
// .frame(height: 36) .frame(height: 36)
//
// Toggle("Show message status", isOn: $showSentViaProxy) Toggle("Show message status", isOn: $showSentViaProxy)
// } header: { } header: {
// Text("Private message routing") Text("Private message routing")
// } footer: { } footer: {
// VStack(alignment: .leading) { VStack(alignment: .leading) {
// Text("To protect your IP address, private routing uses your SMP servers to deliver messages.") Text("To protect your IP address, private routing uses your SMP servers to deliver messages.")
// if showSentViaProxy { if showSentViaProxy {
// Text("Show on messages sent via private routing.") Text("Show → on messages sent via private routing.")
// } }
// } }
// } }
Section("Calls") { Section("Calls") {
NavigationLink { NavigationLink {
@@ -16,6 +16,7 @@ struct PrivacySettings: View {
@AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true @AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true
@AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true @AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true
@AppStorage(GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES, store: groupDefaults) private var encryptLocalFiles = true @AppStorage(GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES, store: groupDefaults) private var encryptLocalFiles = true
@AppStorage(GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS, store: groupDefaults) private var askToApproveRelays = true
@State private var simplexLinkMode = privacySimplexLinkModeDefault.get() @State private var simplexLinkMode = privacySimplexLinkModeDefault.get()
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false @AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false @AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
@@ -64,18 +65,6 @@ struct PrivacySettings: View {
} }
Section { Section {
settingsRow("lock.doc") {
Toggle("Encrypt local files", isOn: $encryptLocalFiles)
.onChange(of: encryptLocalFiles) {
setEncryptLocalFiles($0)
}
}
settingsRow("photo") {
Toggle("Auto-accept images", isOn: $autoAcceptImages)
.onChange(of: autoAcceptImages) {
privacyAcceptImagesGroupDefault.set($0)
}
}
settingsRow("network") { settingsRow("network") {
Toggle("Send link previews", isOn: $useLinkPreviews) Toggle("Send link previews", isOn: $useLinkPreviews)
} }
@@ -108,6 +97,32 @@ struct PrivacySettings: View {
Text("Chats") Text("Chats")
} }
Section {
settingsRow("lock.doc") {
Toggle("Encrypt local files", isOn: $encryptLocalFiles)
.onChange(of: encryptLocalFiles) {
setEncryptLocalFiles($0)
}
}
settingsRow("photo") {
Toggle("Auto-accept images", isOn: $autoAcceptImages)
.onChange(of: autoAcceptImages) {
privacyAcceptImagesGroupDefault.set($0)
}
}
settingsRow("network.badge.shield.half.filled") {
Toggle("Protect IP address", isOn: $askToApproveRelays)
}
} header: {
Text("Files")
} footer: {
if askToApproveRelays {
Text("The app will ask to confirm downloads from unknown file servers (except .onion).")
} else {
Text("Without Tor or VPN, your IP address will be visible to file servers.")
}
}
Section { Section {
settingsRow("person") { settingsRow("person") {
Toggle("Contacts", isOn: $contactReceipts) Toggle("Contacts", isOn: $contactReceipts)
@@ -696,14 +696,16 @@ func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? {
} }
func apiReceiveFile(fileId: Int64, encrypted: Bool, inline: Bool? = nil) -> AChatItem? { func apiReceiveFile(fileId: Int64, encrypted: Bool, inline: Bool? = nil) -> AChatItem? {
let r = sendSimpleXCmd(.receiveFile(fileId: fileId, encrypted: encrypted, inline: inline)) let userApprovedRelays = !privacyAskToApproveRelaysGroupDefault.get()
let r = sendSimpleXCmd(.receiveFile(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted, inline: inline))
if case let .rcvFileAccepted(_, chatItem) = r { return chatItem } if case let .rcvFileAccepted(_, chatItem) = r { return chatItem }
logger.error("receiveFile error: \(responseError(r))") logger.error("receiveFile error: \(responseError(r))")
return nil return nil
} }
func apiSetFileToReceive(fileId: Int64, encrypted: Bool) { func apiSetFileToReceive(fileId: Int64, encrypted: Bool) {
let r = sendSimpleXCmd(.setFileToReceive(fileId: fileId, encrypted: encrypted)) let userApprovedRelays = !privacyAskToApproveRelaysGroupDefault.get()
let r = sendSimpleXCmd(.setFileToReceive(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted))
if case .cmdOk = r { return } if case .cmdOk = r { return }
logger.error("setFileToReceive error: \(responseError(r))") logger.error("setFileToReceive error: \(responseError(r))")
} }
+26 -26
View File
@@ -76,11 +76,11 @@
5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */; }; 5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */; };
5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */; }; 5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */; };
5C9D811A2AA8727A001D49FD /* CryptoFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */; }; 5C9D811A2AA8727A001D49FD /* CryptoFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */; };
5C9F3DCC2BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DC72BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g.a */; }; 5C9F3DD62BFBCDD90003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DD12BFBCDD80003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf-ghc9.6.3.a */; };
5C9F3DCD2BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DC82BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g-ghc9.6.3.a */; }; 5C9F3DD72BFBCDD90003B86B /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DD22BFBCDD80003B86B /* libffi.a */; };
5C9F3DCE2BF7A6900003B86B /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DC92BF7A6900003B86B /* libgmp.a */; }; 5C9F3DD82BFBCDD90003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DD32BFBCDD80003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf.a */; };
5C9F3DCF2BF7A6900003B86B /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DCA2BF7A6900003B86B /* libgmpxx.a */; }; 5C9F3DD92BFBCDD90003B86B /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DD42BFBCDD90003B86B /* libgmpxx.a */; };
5C9F3DD02BF7A6900003B86B /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DCB2BF7A6900003B86B /* libffi.a */; }; 5C9F3DDA2BFBCDD90003B86B /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DD52BFBCDD90003B86B /* libgmp.a */; };
5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */; }; 5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */; };
5CA059DC279559F40002BEB4 /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DB279559F40002BEB4 /* Tests_iOS.swift */; }; 5CA059DC279559F40002BEB4 /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DB279559F40002BEB4 /* Tests_iOS.swift */; };
5CA059DE279559F40002BEB4 /* Tests_iOSLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DD279559F40002BEB4 /* Tests_iOSLaunchTests.swift */; }; 5CA059DE279559F40002BEB4 /* Tests_iOSLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DD279559F40002BEB4 /* Tests_iOSLaunchTests.swift */; };
@@ -354,11 +354,11 @@
5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseEncryptionView.swift; sourceTree = "<group>"; }; 5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseEncryptionView.swift; sourceTree = "<group>"; };
5C9D13A2282187BB00AB8B43 /* WebRTC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTC.swift; sourceTree = "<group>"; }; 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTC.swift; sourceTree = "<group>"; };
5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoFile.swift; sourceTree = "<group>"; }; 5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoFile.swift; sourceTree = "<group>"; };
5C9F3DC72BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g.a"; sourceTree = "<group>"; }; 5C9F3DD12BFBCDD80003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf-ghc9.6.3.a"; sourceTree = "<group>"; };
5C9F3DC82BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g-ghc9.6.3.a"; sourceTree = "<group>"; }; 5C9F3DD22BFBCDD80003B86B /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5C9F3DC92BF7A6900003B86B /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; }; 5C9F3DD32BFBCDD80003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf.a"; sourceTree = "<group>"; };
5C9F3DCA2BF7A6900003B86B /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; }; 5C9F3DD42BFBCDD90003B86B /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5C9F3DCB2BF7A6900003B86B /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; }; 5C9F3DD52BFBCDD90003B86B /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5C9FD96A27A56D4D0075386C /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = "<group>"; }; 5C9FD96A27A56D4D0075386C /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = "<group>"; };
5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageView.swift; sourceTree = "<group>"; }; 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageView.swift; sourceTree = "<group>"; };
5CA059C3279559F40002BEB4 /* SimpleXApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXApp.swift; sourceTree = "<group>"; }; 5CA059C3279559F40002BEB4 /* SimpleXApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXApp.swift; sourceTree = "<group>"; };
@@ -529,13 +529,13 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
5C9F3DCF2BF7A6900003B86B /* libgmpxx.a in Frameworks */, 5C9F3DD92BFBCDD90003B86B /* libgmpxx.a in Frameworks */,
5C9F3DDA2BFBCDD90003B86B /* libgmp.a in Frameworks */,
5C9F3DD82BFBCDD90003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
5C9F3DCE2BF7A6900003B86B /* libgmp.a in Frameworks */, 5C9F3DD62BFBCDD90003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf-ghc9.6.3.a in Frameworks */,
5C9F3DCC2BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g.a in Frameworks */,
5C9F3DD02BF7A6900003B86B /* libffi.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
5C9F3DCD2BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g-ghc9.6.3.a in Frameworks */, 5C9F3DD72BFBCDD90003B86B /* libffi.a in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -601,11 +601,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = { 5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
5C9F3DCB2BF7A6900003B86B /* libffi.a */, 5C9F3DD22BFBCDD80003B86B /* libffi.a */,
5C9F3DC92BF7A6900003B86B /* libgmp.a */, 5C9F3DD52BFBCDD90003B86B /* libgmp.a */,
5C9F3DCA2BF7A6900003B86B /* libgmpxx.a */, 5C9F3DD42BFBCDD90003B86B /* libgmpxx.a */,
5C9F3DC82BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g-ghc9.6.3.a */, 5C9F3DD12BFBCDD80003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf-ghc9.6.3.a */,
5C9F3DC72BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g.a */, 5C9F3DD32BFBCDD80003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf.a */,
); );
path = Libraries; path = Libraries;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1552,7 +1552,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 217; CURRENT_PROJECT_VERSION = 218;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@@ -1601,7 +1601,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 217; CURRENT_PROJECT_VERSION = 218;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@@ -1687,7 +1687,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 217; CURRENT_PROJECT_VERSION = 218;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
GCC_OPTIMIZATION_LEVEL = s; GCC_OPTIMIZATION_LEVEL = s;
@@ -1724,7 +1724,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 217; CURRENT_PROJECT_VERSION = 218;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
ENABLE_CODE_COVERAGE = NO; ENABLE_CODE_COVERAGE = NO;
@@ -1761,7 +1761,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 217; CURRENT_PROJECT_VERSION = 218;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1812,7 +1812,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 217; CURRENT_PROJECT_VERSION = 218;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
+8 -15
View File
@@ -123,8 +123,8 @@ public enum ChatCommand {
case apiGetNetworkStatuses case apiGetNetworkStatuses
case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64)) case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64))
case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool)
case receiveFile(fileId: Int64, encrypted: Bool?, inline: Bool?) case receiveFile(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool?, inline: Bool?)
case setFileToReceive(fileId: Int64, encrypted: Bool?) case setFileToReceive(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool?)
case cancelFile(fileId: Int64) case cancelFile(fileId: Int64)
// remote desktop commands // remote desktop commands
case setLocalDeviceName(displayName: String) case setLocalDeviceName(displayName: String)
@@ -140,7 +140,6 @@ public enum ChatCommand {
case apiStandaloneFileInfo(url: String) case apiStandaloneFileInfo(url: String)
// misc // misc
case showVersion case showVersion
case setAppLogLevel(appLogLevel: ChatLogLevel)
case string(String) case string(String)
public var cmdString: String { public var cmdString: String {
@@ -283,8 +282,8 @@ public enum ChatCommand {
case .apiGetNetworkStatuses: return "/_network_statuses" case .apiGetNetworkStatuses: return "/_network_statuses"
case let .apiChatRead(type, id, itemRange: (from, to)): return "/_read chat \(ref(type, id)) from=\(from) to=\(to)" case let .apiChatRead(type, id, itemRange: (from, to)): return "/_read chat \(ref(type, id)) from=\(from) to=\(to)"
case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id)) \(onOff(unreadChat))" case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id)) \(onOff(unreadChat))"
case let .receiveFile(fileId, encrypt, inline): return "/freceive \(fileId)\(onOffParam("encrypt", encrypt))\(onOffParam("inline", inline))" case let .receiveFile(fileId, userApprovedRelays, encrypt, inline): return "/freceive \(fileId)\(onOffParam("approved_relays", userApprovedRelays))\(onOffParam("encrypt", encrypt))\(onOffParam("inline", inline))"
case let .setFileToReceive(fileId, encrypt): return "/_set_file_to_receive \(fileId)\(onOffParam("encrypt", encrypt))" case let .setFileToReceive(fileId, userApprovedRelays, encrypt): return "/_set_file_to_receive \(fileId)\(onOffParam("approved_relays", userApprovedRelays))\(onOffParam("encrypt", encrypt))"
case let .cancelFile(fileId): return "/fcancel \(fileId)" case let .cancelFile(fileId): return "/fcancel \(fileId)"
case let .setLocalDeviceName(displayName): return "/set device name \(displayName)" case let .setLocalDeviceName(displayName): return "/set device name \(displayName)"
case let .connectRemoteCtrl(xrcpInv): return "/connect remote ctrl \(xrcpInv)" case let .connectRemoteCtrl(xrcpInv): return "/connect remote ctrl \(xrcpInv)"
@@ -298,7 +297,6 @@ public enum ChatCommand {
case let .apiDownloadStandaloneFile(userId, link, file): return "/_download \(userId) \(link) \(file.filePath)" case let .apiDownloadStandaloneFile(userId, link, file): return "/_download \(userId) \(link) \(file.filePath)"
case let .apiStandaloneFileInfo(link): return "/_download info \(link)" case let .apiStandaloneFileInfo(link): return "/_download info \(link)"
case .showVersion: return "/version" case .showVersion: return "/version"
case let .setAppLogLevel(ll): return "/log \(ll.rawValue)"
case let .string(str): return str case let .string(str): return str
} }
} }
@@ -431,7 +429,6 @@ public enum ChatCommand {
case .apiDownloadStandaloneFile: return "apiDownloadStandaloneFile" case .apiDownloadStandaloneFile: return "apiDownloadStandaloneFile"
case .apiStandaloneFileInfo: return "apiStandaloneFileInfo" case .apiStandaloneFileInfo: return "apiStandaloneFileInfo"
case .showVersion: return "showVersion" case .showVersion: return "showVersion"
case .setAppLogLevel: return "setAppLogLevel"
case .string: return "console command" case .string: return "console command"
} }
} }
@@ -1763,6 +1760,7 @@ public enum ChatErrorType: Decodable {
case fileImageType(filePath: String) case fileImageType(filePath: String)
case fileImageSize(filePath: String) case fileImageSize(filePath: String)
case fileNotReceived(fileId: Int64) case fileNotReceived(fileId: Int64)
case fileNotApproved(fileId: Int64, unknownServers: [String])
// case xFTPRcvFile // case xFTPRcvFile
// case xFTPSndFile // case xFTPSndFile
case fallbackToSMPProhibited(fileId: Int64) case fallbackToSMPProhibited(fileId: Int64)
@@ -2041,6 +2039,7 @@ public struct MigrationFileLinkData: Codable {
public struct AppSettings: Codable, Equatable { public struct AppSettings: Codable, Equatable {
public var networkConfig: NetCfg? = nil public var networkConfig: NetCfg? = nil
public var privacyEncryptLocalFiles: Bool? = nil public var privacyEncryptLocalFiles: Bool? = nil
public var privacyAskToApproveRelays: Bool? = nil
public var privacyAcceptImages: Bool? = nil public var privacyAcceptImages: Bool? = nil
public var privacyLinkPreviews: Bool? = nil public var privacyLinkPreviews: Bool? = nil
public var privacyShowChatPreviews: Bool? = nil public var privacyShowChatPreviews: Bool? = nil
@@ -2064,6 +2063,7 @@ public struct AppSettings: Codable, Equatable {
let def = AppSettings.defaults let def = AppSettings.defaults
if networkConfig != def.networkConfig { empty.networkConfig = networkConfig } if networkConfig != def.networkConfig { empty.networkConfig = networkConfig }
if privacyEncryptLocalFiles != def.privacyEncryptLocalFiles { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles } if privacyEncryptLocalFiles != def.privacyEncryptLocalFiles { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles }
if privacyAskToApproveRelays != def.privacyAskToApproveRelays { empty.privacyAskToApproveRelays = privacyAskToApproveRelays }
if privacyAcceptImages != def.privacyAcceptImages { empty.privacyAcceptImages = privacyAcceptImages } if privacyAcceptImages != def.privacyAcceptImages { empty.privacyAcceptImages = privacyAcceptImages }
if privacyLinkPreviews != def.privacyLinkPreviews { empty.privacyLinkPreviews = privacyLinkPreviews } if privacyLinkPreviews != def.privacyLinkPreviews { empty.privacyLinkPreviews = privacyLinkPreviews }
if privacyShowChatPreviews != def.privacyShowChatPreviews { empty.privacyShowChatPreviews = privacyShowChatPreviews } if privacyShowChatPreviews != def.privacyShowChatPreviews { empty.privacyShowChatPreviews = privacyShowChatPreviews }
@@ -2088,6 +2088,7 @@ public struct AppSettings: Codable, Equatable {
AppSettings ( AppSettings (
networkConfig: NetCfg.defaults, networkConfig: NetCfg.defaults,
privacyEncryptLocalFiles: true, privacyEncryptLocalFiles: true,
privacyAskToApproveRelays: true,
privacyAcceptImages: true, privacyAcceptImages: true,
privacyLinkPreviews: true, privacyLinkPreviews: true,
privacyShowChatPreviews: true, privacyShowChatPreviews: true,
@@ -2170,11 +2171,3 @@ public enum UserNetworkType: String, Codable {
} }
} }
} }
public enum ChatLogLevel: String, Codable, CaseIterable {
case debug
case info
case warn
case error
case important
}
+4 -8
View File
@@ -23,6 +23,7 @@ public let GROUP_DEFAULT_NTF_ENABLE_PERIODIC = "ntfEnablePeriodic" // no longer
let GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages" let GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages"
public let GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE = "privacyTransferImagesInline" // no longer used public let GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE = "privacyTransferImagesInline" // no longer used
public let GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES = "privacyEncryptLocalFiles" public let GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES = "privacyEncryptLocalFiles"
public let GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS = "privacyAskToApproveRelays"
let GROUP_DEFAULT_NTF_BADGE_COUNT = "ntgBadgeCount" let GROUP_DEFAULT_NTF_BADGE_COUNT = "ntgBadgeCount"
let GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS = "networkUseOnionHosts" let GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS = "networkUseOnionHosts"
let GROUP_DEFAULT_NETWORK_SESSION_MODE = "networkSessionMode" let GROUP_DEFAULT_NETWORK_SESSION_MODE = "networkSessionMode"
@@ -44,7 +45,6 @@ public let GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE = "initialRandomDBPassphra
public let GROUP_DEFAULT_CONFIRM_DB_UPGRADES = "confirmDBUpgrades" public let GROUP_DEFAULT_CONFIRM_DB_UPGRADES = "confirmDBUpgrades"
public let GROUP_DEFAULT_CALL_KIT_ENABLED = "callKitEnabled" public let GROUP_DEFAULT_CALL_KIT_ENABLED = "callKitEnabled"
public let GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED = "pqExperimentalEnabled" // no longer used public let GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED = "pqExperimentalEnabled" // no longer used
public let GROUP_DEFAULT_APP_LOG_LEVEL = "appLogLevel"
public let APP_GROUP_NAME = "group.chat.simplex.app" public let APP_GROUP_NAME = "group.chat.simplex.app"
@@ -74,10 +74,10 @@ public func registerGroupDefaults() {
GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES: true, GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES: true,
GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE: false, GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE: false,
GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES: true, GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES: true,
GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS: true,
GROUP_DEFAULT_CONFIRM_DB_UPGRADES: false, GROUP_DEFAULT_CONFIRM_DB_UPGRADES: false,
GROUP_DEFAULT_CALL_KIT_ENABLED: true, GROUP_DEFAULT_CALL_KIT_ENABLED: true,
GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED: false, GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED: false,
GROUP_DEFAULT_APP_LOG_LEVEL: ChatLogLevel.important.rawValue,
]) ])
} }
@@ -183,6 +183,8 @@ public let privacyAcceptImagesGroupDefault = BoolDefault(defaults: groupDefaults
public let privacyEncryptLocalFilesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES) public let privacyEncryptLocalFilesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES)
public let privacyAskToApproveRelaysGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS)
public let ntfBadgeCountGroupDefault = IntDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_NTF_BADGE_COUNT) public let ntfBadgeCountGroupDefault = IntDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_NTF_BADGE_COUNT)
public let networkUseOnionHostsGroupDefault = EnumDefault<OnionHosts>( public let networkUseOnionHostsGroupDefault = EnumDefault<OnionHosts>(
@@ -217,12 +219,6 @@ public let confirmDBUpgradesGroupDefault = BoolDefault(defaults: groupDefaults,
public let callKitEnabledGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_CALL_KIT_ENABLED) public let callKitEnabledGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_CALL_KIT_ENABLED)
public let appLogLevelGroupDefault = EnumDefault<ChatLogLevel>(
defaults: groupDefaults,
forKey: GROUP_DEFAULT_APP_LOG_LEVEL,
withDefault: .important
)
public class DateDefault { public class DateDefault {
var defaults: UserDefaults var defaults: UserDefaults
var key: String var key: String
+4
View File
@@ -3174,6 +3174,7 @@ public struct CIFile: Decodable {
case .rcvInvitation: return false case .rcvInvitation: return false
case .rcvAccepted: return false case .rcvAccepted: return false
case .rcvTransfer: return false case .rcvTransfer: return false
case .rcvAborted: return false
case .rcvCancelled: return false case .rcvCancelled: return false
case .rcvComplete: return true case .rcvComplete: return true
case .rcvError: return false case .rcvError: return false
@@ -3198,6 +3199,7 @@ public struct CIFile: Decodable {
case .rcvInvitation: return nil case .rcvInvitation: return nil
case .rcvAccepted: return rcvCancelAction case .rcvAccepted: return rcvCancelAction
case .rcvTransfer: return rcvCancelAction case .rcvTransfer: return rcvCancelAction
case .rcvAborted: return nil
case .rcvCancelled: return nil case .rcvCancelled: return nil
case .rcvComplete: return nil case .rcvComplete: return nil
case .rcvError: return nil case .rcvError: return nil
@@ -3312,6 +3314,7 @@ public enum CIFileStatus: Decodable, Equatable {
case rcvInvitation case rcvInvitation
case rcvAccepted case rcvAccepted
case rcvTransfer(rcvProgress: Int64, rcvTotal: Int64) case rcvTransfer(rcvProgress: Int64, rcvTotal: Int64)
case rcvAborted
case rcvComplete case rcvComplete
case rcvCancelled case rcvCancelled
case rcvError case rcvError
@@ -3327,6 +3330,7 @@ public enum CIFileStatus: Decodable, Equatable {
case .rcvInvitation: return "rcvInvitation" case .rcvInvitation: return "rcvInvitation"
case .rcvAccepted: return "rcvAccepted" case .rcvAccepted: return "rcvAccepted"
case let .rcvTransfer(rcvProgress, rcvTotal): return "rcvTransfer \(rcvProgress) \(rcvTotal)" case let .rcvTransfer(rcvProgress, rcvTotal): return "rcvTransfer \(rcvProgress) \(rcvTotal)"
case .rcvAborted: return "rcvAborted"
case .rcvComplete: return "rcvComplete" case .rcvComplete: return "rcvComplete"
case .rcvCancelled: return "rcvCancelled" case .rcvCancelled: return "rcvCancelled"
case .rcvError: return "rcvError" case .rcvError: return "rcvError"
@@ -2644,6 +2644,7 @@ data class CIFile(
is CIFileStatus.RcvInvitation -> false is CIFileStatus.RcvInvitation -> false
is CIFileStatus.RcvAccepted -> false is CIFileStatus.RcvAccepted -> false
is CIFileStatus.RcvTransfer -> false is CIFileStatus.RcvTransfer -> false
is CIFileStatus.RcvAborted -> false
is CIFileStatus.RcvCancelled -> false is CIFileStatus.RcvCancelled -> false
is CIFileStatus.RcvComplete -> true is CIFileStatus.RcvComplete -> true
is CIFileStatus.RcvError -> false is CIFileStatus.RcvError -> false
@@ -2665,6 +2666,7 @@ data class CIFile(
is CIFileStatus.RcvInvitation -> null is CIFileStatus.RcvInvitation -> null
is CIFileStatus.RcvAccepted -> rcvCancelAction is CIFileStatus.RcvAccepted -> rcvCancelAction
is CIFileStatus.RcvTransfer -> rcvCancelAction is CIFileStatus.RcvTransfer -> rcvCancelAction
is CIFileStatus.RcvAborted -> null
is CIFileStatus.RcvCancelled -> null is CIFileStatus.RcvCancelled -> null
is CIFileStatus.RcvComplete -> null is CIFileStatus.RcvComplete -> null
is CIFileStatus.RcvError -> null is CIFileStatus.RcvError -> null
@@ -2845,6 +2847,7 @@ sealed class CIFileStatus {
@Serializable @SerialName("rcvInvitation") object RcvInvitation: CIFileStatus() @Serializable @SerialName("rcvInvitation") object RcvInvitation: CIFileStatus()
@Serializable @SerialName("rcvAccepted") object RcvAccepted: CIFileStatus() @Serializable @SerialName("rcvAccepted") object RcvAccepted: CIFileStatus()
@Serializable @SerialName("rcvTransfer") class RcvTransfer(val rcvProgress: Long, val rcvTotal: Long): CIFileStatus() @Serializable @SerialName("rcvTransfer") class RcvTransfer(val rcvProgress: Long, val rcvTotal: Long): CIFileStatus()
@Serializable @SerialName("rcvAborted") object RcvAborted: CIFileStatus()
@Serializable @SerialName("rcvComplete") object RcvComplete: CIFileStatus() @Serializable @SerialName("rcvComplete") object RcvComplete: CIFileStatus()
@Serializable @SerialName("rcvCancelled") object RcvCancelled: CIFileStatus() @Serializable @SerialName("rcvCancelled") object RcvCancelled: CIFileStatus()
@Serializable @SerialName("rcvError") object RcvError: CIFileStatus() @Serializable @SerialName("rcvError") object RcvError: CIFileStatus()
@@ -2859,6 +2862,7 @@ sealed class CIFileStatus {
is RcvInvitation -> false is RcvInvitation -> false
is RcvAccepted -> false is RcvAccepted -> false
is RcvTransfer -> false is RcvTransfer -> false
is RcvAborted -> false
is RcvComplete -> false is RcvComplete -> false
is RcvCancelled -> false is RcvCancelled -> false
is RcvError -> false is RcvError -> false
@@ -12,6 +12,7 @@ import dev.icerock.moko.resources.compose.painterResource
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.* import chat.simplex.common.views.call.*
import chat.simplex.common.views.chat.group.toggleShowMemberMessages
import chat.simplex.common.views.migration.MigrationFileLinkData import chat.simplex.common.views.migration.MigrationFileLinkData
import chat.simplex.common.views.onboarding.OnboardingStage import chat.simplex.common.views.onboarding.OnboardingStage
import chat.simplex.common.views.usersettings.* import chat.simplex.common.views.usersettings.*
@@ -106,6 +107,7 @@ class AppPreferences {
val privacySaveLastDraft = mkBoolPreference(SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT, true) val privacySaveLastDraft = mkBoolPreference(SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT, true)
val privacyDeliveryReceiptsSet = mkBoolPreference(SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET, false) val privacyDeliveryReceiptsSet = mkBoolPreference(SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET, false)
val privacyEncryptLocalFiles = mkBoolPreference(SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES, true) val privacyEncryptLocalFiles = mkBoolPreference(SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES, true)
val privacyAskToApproveRelays = mkBoolPreference(SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS, true)
val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false) val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false)
val showUnreadAndFavorites = mkBoolPreference(SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES, false) val showUnreadAndFavorites = mkBoolPreference(SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES, false)
val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null) val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null)
@@ -292,6 +294,7 @@ class AppPreferences {
private const val SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT = "PrivacySaveLastDraft" private const val SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT = "PrivacySaveLastDraft"
private const val SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET = "PrivacyDeliveryReceiptsSet" private const val SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET = "PrivacyDeliveryReceiptsSet"
private const val SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES = "PrivacyEncryptLocalFiles" private const val SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES = "PrivacyEncryptLocalFiles"
private const val SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS = "PrivacyAskToApproveRelays"
const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup" const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup"
private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls" private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls"
private const val SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES = "ShowUnreadAndFavorites" private const val SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES = "ShowUnreadAndFavorites"
@@ -1337,9 +1340,9 @@ object ChatController {
} }
} }
suspend fun apiReceiveFile(rh: Long?, fileId: Long, encrypted: Boolean, inline: Boolean? = null, auto: Boolean = false): AChatItem? { suspend fun apiReceiveFile(rh: Long?, fileId: Long, userApprovedRelays: Boolean, encrypted: Boolean, inline: Boolean? = null, auto: Boolean = false): AChatItem? {
// -1 here is to override default behavior of providing current remote host id because file can be asked by local device while remote is connected // -1 here is to override default behavior of providing current remote host id because file can be asked by local device while remote is connected
val r = sendCmd(rh, CC.ReceiveFile(fileId, encrypted, inline)) val r = sendCmd(rh, CC.ReceiveFile(fileId, userApprovedRelays = userApprovedRelays, encrypt = encrypted, inline = inline))
return when (r) { return when (r) {
is CR.RcvFileAccepted -> r.chatItem is CR.RcvFileAccepted -> r.chatItem
is CR.RcvFileAcceptedSndCancelled -> { is CR.RcvFileAcceptedSndCancelled -> {
@@ -1358,7 +1361,23 @@ object ChatController {
val maybeChatError = chatError(r) val maybeChatError = chatError(r)
if (maybeChatError is ChatErrorType.FileCancelled || maybeChatError is ChatErrorType.FileAlreadyReceiving) { if (maybeChatError is ChatErrorType.FileCancelled || maybeChatError is ChatErrorType.FileAlreadyReceiving) {
Log.d(TAG, "apiReceiveFile ignoring FileCancelled or FileAlreadyReceiving error") Log.d(TAG, "apiReceiveFile ignoring FileCancelled or FileAlreadyReceiving error")
} else { } else if (maybeChatError is ChatErrorType.FileNotApproved) {
Log.d(TAG, "apiReceiveFile FileNotApproved error")
if (!auto) {
val srvs = maybeChatError.unknownServers.map{ serverHostname(it) }
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.file_not_approved_title),
text = generalGetString(MR.strings.file_not_approved_descr).format(srvs.sorted().joinToString(separator = ", ")),
confirmText = generalGetString(MR.strings.download_file),
onConfirm = {
val user = chatModel.currentUser.value
if (user != null) {
withBGApi { chatModel.controller.receiveFile(rh, user, fileId, userApprovedRelays = true) }
}
},
)
}
} else if (!auto) {
apiErrorAlert("apiReceiveFile", generalGetString(MR.strings.error_receiving_file), r) apiErrorAlert("apiReceiveFile", generalGetString(MR.strings.error_receiving_file), r)
} }
} }
@@ -2216,9 +2235,14 @@ object ChatController {
} }
} }
suspend fun receiveFile(rhId: Long?, user: UserLike, fileId: Long, auto: Boolean = false) { suspend fun receiveFile(rhId: Long?, user: UserLike, fileId: Long, userApprovedRelays: Boolean = false, auto: Boolean = false) {
val encrypted = appPrefs.privacyEncryptLocalFiles.get() val chatItem = apiReceiveFile(
val chatItem = apiReceiveFile(rhId, fileId, encrypted = encrypted, auto = auto) rhId,
fileId,
userApprovedRelays = userApprovedRelays || !appPrefs.privacyAskToApproveRelays.get(),
encrypted = appPrefs.privacyEncryptLocalFiles.get(),
auto = auto
)
if (chatItem != null) { if (chatItem != null) {
chatItemSimpleUpdate(rhId, user, chatItem) chatItemSimpleUpdate(rhId, user, chatItem)
} }
@@ -2501,7 +2525,7 @@ sealed class CC {
class ApiRejectContact(val contactReqId: Long): CC() class ApiRejectContact(val contactReqId: Long): CC()
class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC() class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC()
class ApiChatUnread(val type: ChatType, val id: Long, val unreadChat: Boolean): CC() class ApiChatUnread(val type: ChatType, val id: Long, val unreadChat: Boolean): CC()
class ReceiveFile(val fileId: Long, val encrypt: Boolean, val inline: Boolean?): CC() class ReceiveFile(val fileId: Long, val userApprovedRelays: Boolean, val encrypt: Boolean, val inline: Boolean?): CC()
class CancelFile(val fileId: Long): CC() class CancelFile(val fileId: Long): CC()
// Remote control // Remote control
class SetLocalDeviceName(val displayName: String): CC() class SetLocalDeviceName(val displayName: String): CC()
@@ -2652,6 +2676,7 @@ sealed class CC {
is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}" is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}"
is ReceiveFile -> is ReceiveFile ->
"/freceive $fileId" + "/freceive $fileId" +
(" approved_relays=${onOff(userApprovedRelays)}") +
(if (encrypt == null) "" else " encrypt=${onOff(encrypt)}") + (if (encrypt == null) "" else " encrypt=${onOff(encrypt)}") +
(if (inline == null) "" else " inline=${onOff(inline)}") (if (inline == null) "" else " inline=${onOff(inline)}")
is CancelFile -> "/fcancel $fileId" is CancelFile -> "/fcancel $fileId"
@@ -4892,13 +4917,14 @@ sealed class ChatErrorType {
is FileCancel -> "fileCancel" is FileCancel -> "fileCancel"
is FileAlreadyExists -> "fileAlreadyExists" is FileAlreadyExists -> "fileAlreadyExists"
is FileRead -> "fileRead" is FileRead -> "fileRead"
is FileWrite -> "fileWrite" is FileWrite -> "fileWrite $message"
is FileSend -> "fileSend" is FileSend -> "fileSend"
is FileRcvChunk -> "fileRcvChunk" is FileRcvChunk -> "fileRcvChunk"
is FileInternal -> "fileInternal" is FileInternal -> "fileInternal"
is FileImageType -> "fileImageType" is FileImageType -> "fileImageType"
is FileImageSize -> "fileImageSize" is FileImageSize -> "fileImageSize"
is FileNotReceived -> "fileNotReceived" is FileNotReceived -> "fileNotReceived"
is FileNotApproved -> "fileNotApproved"
// is XFTPRcvFile -> "xftpRcvFile" // is XFTPRcvFile -> "xftpRcvFile"
// is XFTPSndFile -> "xftpSndFile" // is XFTPSndFile -> "xftpSndFile"
is FallbackToSMPProhibited -> "fallbackToSMPProhibited" is FallbackToSMPProhibited -> "fallbackToSMPProhibited"
@@ -4978,6 +5004,7 @@ sealed class ChatErrorType {
@Serializable @SerialName("fileImageType") class FileImageType(val filePath: String): ChatErrorType() @Serializable @SerialName("fileImageType") class FileImageType(val filePath: String): ChatErrorType()
@Serializable @SerialName("fileImageSize") class FileImageSize(val filePath: String): ChatErrorType() @Serializable @SerialName("fileImageSize") class FileImageSize(val filePath: String): ChatErrorType()
@Serializable @SerialName("fileNotReceived") class FileNotReceived(val fileId: Long): ChatErrorType() @Serializable @SerialName("fileNotReceived") class FileNotReceived(val fileId: Long): ChatErrorType()
@Serializable @SerialName("fileNotApproved") class FileNotApproved(val fileId: Long, val unknownServers: List<String>): ChatErrorType()
// @Serializable @SerialName("xFTPRcvFile") object XFTPRcvFile: ChatErrorType() // @Serializable @SerialName("xFTPRcvFile") object XFTPRcvFile: ChatErrorType()
// @Serializable @SerialName("xFTPSndFile") object XFTPSndFile: ChatErrorType() // @Serializable @SerialName("xFTPSndFile") object XFTPSndFile: ChatErrorType()
@Serializable @SerialName("fallbackToSMPProhibited") class FallbackToSMPProhibited(val fileId: Long): ChatErrorType() @Serializable @SerialName("fallbackToSMPProhibited") class FallbackToSMPProhibited(val fileId: Long): ChatErrorType()
@@ -5476,6 +5503,7 @@ enum class NotificationsMode() {
data class AppSettings( data class AppSettings(
var networkConfig: NetCfg? = null, var networkConfig: NetCfg? = null,
var privacyEncryptLocalFiles: Boolean? = null, var privacyEncryptLocalFiles: Boolean? = null,
var privacyAskToApproveRelays: Boolean? = null,
var privacyAcceptImages: Boolean? = null, var privacyAcceptImages: Boolean? = null,
var privacyLinkPreviews: Boolean? = null, var privacyLinkPreviews: Boolean? = null,
var privacyShowChatPreviews: Boolean? = null, var privacyShowChatPreviews: Boolean? = null,
@@ -5499,6 +5527,7 @@ data class AppSettings(
val def = defaults val def = defaults
if (networkConfig != def.networkConfig) { empty.networkConfig = networkConfig } if (networkConfig != def.networkConfig) { empty.networkConfig = networkConfig }
if (privacyEncryptLocalFiles != def.privacyEncryptLocalFiles) { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles } if (privacyEncryptLocalFiles != def.privacyEncryptLocalFiles) { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles }
if (privacyAskToApproveRelays != def.privacyAskToApproveRelays) { empty.privacyAskToApproveRelays = privacyAskToApproveRelays }
if (privacyAcceptImages != def.privacyAcceptImages) { empty.privacyAcceptImages = privacyAcceptImages } if (privacyAcceptImages != def.privacyAcceptImages) { empty.privacyAcceptImages = privacyAcceptImages }
if (privacyLinkPreviews != def.privacyLinkPreviews) { empty.privacyLinkPreviews = privacyLinkPreviews } if (privacyLinkPreviews != def.privacyLinkPreviews) { empty.privacyLinkPreviews = privacyLinkPreviews }
if (privacyShowChatPreviews != def.privacyShowChatPreviews) { empty.privacyShowChatPreviews = privacyShowChatPreviews } if (privacyShowChatPreviews != def.privacyShowChatPreviews) { empty.privacyShowChatPreviews = privacyShowChatPreviews }
@@ -5530,6 +5559,7 @@ data class AppSettings(
setNetCfg(net) setNetCfg(net)
} }
privacyEncryptLocalFiles?.let { def.privacyEncryptLocalFiles.set(it) } privacyEncryptLocalFiles?.let { def.privacyEncryptLocalFiles.set(it) }
privacyAskToApproveRelays?.let { def.privacyAskToApproveRelays.set(it) }
privacyAcceptImages?.let { def.privacyAcceptImages.set(it) } privacyAcceptImages?.let { def.privacyAcceptImages.set(it) }
privacyLinkPreviews?.let { def.privacyLinkPreviews.set(it) } privacyLinkPreviews?.let { def.privacyLinkPreviews.set(it) }
privacyShowChatPreviews?.let { def.privacyShowChatPreviews.set(it) } privacyShowChatPreviews?.let { def.privacyShowChatPreviews.set(it) }
@@ -5554,6 +5584,7 @@ data class AppSettings(
get() = AppSettings( get() = AppSettings(
networkConfig = NetCfg.defaults, networkConfig = NetCfg.defaults,
privacyEncryptLocalFiles = true, privacyEncryptLocalFiles = true,
privacyAskToApproveRelays = true,
privacyAcceptImages = true, privacyAcceptImages = true,
privacyLinkPreviews = true, privacyLinkPreviews = true,
privacyShowChatPreviews = true, privacyShowChatPreviews = true,
@@ -5579,6 +5610,7 @@ data class AppSettings(
return defaults.copy( return defaults.copy(
networkConfig = getNetCfg(), networkConfig = getNetCfg(),
privacyEncryptLocalFiles = def.privacyEncryptLocalFiles.get(), privacyEncryptLocalFiles = def.privacyEncryptLocalFiles.get(),
privacyAskToApproveRelays = def.privacyAskToApproveRelays.get(),
privacyAcceptImages = def.privacyAcceptImages.get(), privacyAcceptImages = def.privacyAcceptImages.get(),
privacyLinkPreviews = def.privacyLinkPreviews.get(), privacyLinkPreviews = def.privacyLinkPreviews.get(),
privacyShowChatPreviews = def.privacyShowChatPreviews.get(), privacyShowChatPreviews = def.privacyShowChatPreviews.get(),
@@ -1,6 +1,5 @@
package chat.simplex.common.views.chat.item package chat.simplex.common.views.chat.item
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.CornerSize
@@ -64,7 +63,7 @@ fun CIFileView(
fun fileAction() { fun fileAction() {
if (file != null) { if (file != null) {
when { when {
file.fileStatus is CIFileStatus.RcvInvitation -> { file.fileStatus is CIFileStatus.RcvInvitation || file.fileStatus is CIFileStatus.RcvAborted -> {
if (fileSizeValid(file)) { if (fileSizeValid(file)) {
receiveFile(file.fileId) receiveFile(file.fileId)
} else { } else {
@@ -176,6 +175,8 @@ fun CIFileView(
} else { } else {
progressIndicator() progressIndicator()
} }
is CIFileStatus.RcvAborted ->
fileIcon(innerIcon = painterResource(MR.images.ic_sync_problem), color = MaterialTheme.colors.primary)
is CIFileStatus.RcvComplete -> fileIcon() is CIFileStatus.RcvComplete -> fileIcon()
is CIFileStatus.RcvCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) is CIFileStatus.RcvCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
is CIFileStatus.RcvError -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) is CIFileStatus.RcvError -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
@@ -21,17 +21,12 @@ import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.DEFAULT_MAX_IMAGE_WIDTH import chat.simplex.common.ui.theme.DEFAULT_MAX_IMAGE_WIDTH
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.io.File
import java.net.URI
@Composable @Composable
fun CIImageView( fun CIImageView(
image: String, image: String,
file: CIFile?, file: CIFile?,
metaColor: Color,
imageProvider: () -> ImageGalleryProvider, imageProvider: () -> ImageGalleryProvider,
showMenu: MutableState<Boolean>, showMenu: MutableState<Boolean>,
receiveFile: (Long) -> Unit receiveFile: (Long) -> Unit
@@ -51,7 +46,7 @@ fun CIImageView(
icon, icon,
stringResource(stringId), stringResource(stringId),
Modifier.fillMaxSize(), Modifier.fillMaxSize(),
tint = metaColor tint = Color.White
) )
} }
@@ -78,6 +73,7 @@ fun CIImageView(
is CIFileStatus.RcvInvitation -> fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_asked_to_receive) is CIFileStatus.RcvInvitation -> fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_asked_to_receive)
is CIFileStatus.RcvAccepted -> fileIcon(painterResource(MR.images.ic_more_horiz), MR.strings.icon_descr_waiting_for_image) is CIFileStatus.RcvAccepted -> fileIcon(painterResource(MR.images.ic_more_horiz), MR.strings.icon_descr_waiting_for_image)
is CIFileStatus.RcvTransfer -> progressIndicator() is CIFileStatus.RcvTransfer -> progressIndicator()
is CIFileStatus.RcvAborted -> fileIcon(painterResource(MR.images.ic_sync_problem), MR.strings.icon_descr_file)
is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
is CIFileStatus.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) is CIFileStatus.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file) is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file)
@@ -206,7 +202,7 @@ fun CIImageView(
imageView(base64ToBitmap(image), onClick = { imageView(base64ToBitmap(image), onClick = {
if (file != null) { if (file != null) {
when (file.fileStatus) { when (file.fileStatus) {
CIFileStatus.RcvInvitation -> CIFileStatus.RcvInvitation, CIFileStatus.RcvAborted ->
if (fileSizeValid()) { if (fileSizeValid()) {
receiveFile(file.fileId) receiveFile(file.fileId)
} else { } else {
@@ -73,7 +73,7 @@ fun CIVideoView(
VideoPreviewImageView(preview, onClick = { VideoPreviewImageView(preview, onClick = {
if (file != null) { if (file != null) {
when (file.fileStatus) { when (file.fileStatus) {
CIFileStatus.RcvInvitation -> CIFileStatus.RcvInvitation, CIFileStatus.RcvAborted ->
receiveFileIfValidSize(file, receiveFile) receiveFileIfValidSize(file, receiveFile)
CIFileStatus.RcvAccepted -> CIFileStatus.RcvAccepted ->
when (file.fileProtocol) { when (file.fileProtocol) {
@@ -102,7 +102,7 @@ fun CIVideoView(
if (file != null) { if (file != null) {
DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(duration * 1000L) }, remember { mutableStateOf(0L) }/*, soundEnabled*/) DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(duration * 1000L) }, remember { mutableStateOf(0L) }/*, soundEnabled*/)
} }
if (file?.fileStatus is CIFileStatus.RcvInvitation) { if (file?.fileStatus is CIFileStatus.RcvInvitation || file?.fileStatus is CIFileStatus.RcvAborted) {
PlayButton(error = false, { showMenu.value = true }) { receiveFileIfValidSize(file, receiveFile) } PlayButton(error = false, { showMenu.value = true }) { receiveFileIfValidSize(file, receiveFile) }
} }
} }
@@ -396,6 +396,7 @@ private fun loadingIndicator(file: CIFile?) {
} else { } else {
progressIndicator() progressIndicator()
} }
is CIFileStatus.RcvAborted -> fileIcon(painterResource(MR.images.ic_sync_problem), MR.strings.icon_descr_file)
is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
is CIFileStatus.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) is CIFileStatus.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file) is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file)
@@ -22,6 +22,7 @@ import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
// TODO refactor https://github.com/simplex-chat/simplex-chat/pull/1451#discussion_r1033429901 // TODO refactor https://github.com/simplex-chat/simplex-chat/pull/1451#discussion_r1033429901
@@ -220,7 +221,8 @@ private fun PlayPauseButton(
error: Boolean, error: Boolean,
play: () -> Unit, play: () -> Unit,
pause: () -> Unit, pause: () -> Unit,
longClick: () -> Unit longClick: () -> Unit,
icon: ImageResource = MR.images.ic_play_arrow_filled,
) { ) {
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
@@ -241,7 +243,7 @@ private fun PlayPauseButton(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Icon( Icon(
if (audioPlaying) painterResource(MR.images.ic_pause_filled) else painterResource(MR.images.ic_play_arrow_filled), if (audioPlaying) painterResource(MR.images.ic_pause_filled) else painterResource(icon),
contentDescription = null, contentDescription = null,
Modifier.size(36.dp), Modifier.size(36.dp),
tint = if (error) WarningOrange else if (!enabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary tint = if (error) WarningOrange else if (!enabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
@@ -294,6 +296,8 @@ private fun VoiceMsgIndicator(
) { ) {
ProgressIndicator() ProgressIndicator()
} }
} else if (file?.fileStatus is CIFileStatus.RcvAborted) {
PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId) }, {}, longClick = longClick, icon = MR.images.ic_sync_problem)
} else { } else {
PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, false, false, {}, {}, longClick) PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, false, false, {}, {}, longClick)
} }
@@ -241,7 +241,7 @@ fun FramedItemView(
} else { } else {
when (val mc = ci.content.msgContent) { when (val mc = ci.content.msgContent) {
is MsgContent.MCImage -> { is MsgContent.MCImage -> {
CIImageView(image = mc.image, file = ci.file, metaColor = metaColor, imageProvider ?: return@PriorityLayout, showMenu, receiveFile) CIImageView(image = mc.image, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, receiveFile)
if (mc.text == "" && !ci.meta.isLive) { if (mc.text == "" && !ci.meta.isLive) {
metaColor = Color.White metaColor = Color.White
} else { } else {
@@ -250,17 +250,17 @@ fun NetworkAndServersView() {
Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 24.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp)) Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 24.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp))
} }
// if (currentRemoteHost == null) { if (currentRemoteHost == null) {
// SectionView(generalGetString(MR.strings.settings_section_title_private_message_routing)) { SectionView(generalGetString(MR.strings.settings_section_title_private_message_routing)) {
// SMPProxyModePicker(smpProxyMode, showModal, updateSMPProxyMode) SMPProxyModePicker(smpProxyMode, showModal, updateSMPProxyMode)
// SMPProxyFallbackPicker(smpProxyFallback, showModal, updateSMPProxyFallback, enabled = remember { mutableStateOf(smpProxyMode.value != SMPProxyMode.Never) }) SMPProxyFallbackPicker(smpProxyFallback, showModal, updateSMPProxyFallback, enabled = remember { mutableStateOf(smpProxyMode.value != SMPProxyMode.Never) })
// SettingsPreferenceItem(painterResource(MR.images.ic_arrow_forward), stringResource(MR.strings.private_routing_show_message_status), chatModel.controller.appPrefs.showSentViaProxy) SettingsPreferenceItem(painterResource(MR.images.ic_arrow_forward), stringResource(MR.strings.private_routing_show_message_status), chatModel.controller.appPrefs.showSentViaProxy)
// } }
// SectionCustomFooter { SectionCustomFooter {
// Text(stringResource(MR.strings.private_routing_explanation)) Text(stringResource(MR.strings.private_routing_explanation))
// } }
// Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 32.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp)) Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 32.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp))
// } }
SectionView(generalGetString(MR.strings.settings_section_title_calls)) { SectionView(generalGetString(MR.strings.settings_section_title_calls)) {
SettingsActionItem(painterResource(MR.images.ic_electrical_services), stringResource(MR.strings.webrtc_ice_servers), { ModalManager.start.showModal { RTCServersView(m) } }) SettingsActionItem(painterResource(MR.images.ic_electrical_services), stringResource(MR.strings.webrtc_ice_servers), { ModalManager.start.showModal { RTCServersView(m) } })
@@ -1,6 +1,7 @@
package chat.simplex.common.views.usersettings package chat.simplex.common.views.usersettings
import SectionBottomSpacer import SectionBottomSpacer
import SectionCustomFooter
import SectionDividerSpaced import SectionDividerSpaced
import SectionItemView import SectionItemView
import SectionTextFooter import SectionTextFooter
@@ -63,10 +64,6 @@ fun PrivacySettingsView(
SectionDividerSpaced() SectionDividerSpaced()
SectionView(stringResource(MR.strings.settings_section_title_chats)) { SectionView(stringResource(MR.strings.settings_section_title_chats)) {
SettingsPreferenceItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.encrypt_local_files), chatModel.controller.appPrefs.privacyEncryptLocalFiles, onChange = { enable ->
withBGApi { chatModel.controller.apiSetEncryptLocalFiles(enable) }
})
SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages)
SettingsPreferenceItem(painterResource(MR.images.ic_travel_explore), stringResource(MR.strings.send_link_previews), chatModel.controller.appPrefs.privacyLinkPreviews) SettingsPreferenceItem(painterResource(MR.images.ic_travel_explore), stringResource(MR.strings.send_link_previews), chatModel.controller.appPrefs.privacyLinkPreviews)
SettingsPreferenceItem( SettingsPreferenceItem(
painterResource(MR.images.ic_chat_bubble), painterResource(MR.images.ic_chat_bubble),
@@ -91,6 +88,22 @@ fun PrivacySettingsView(
chatModel.simplexLinkMode.value = it chatModel.simplexLinkMode.value = it
}) })
} }
SectionDividerSpaced()
SectionView(stringResource(MR.strings.settings_section_title_files)) {
SettingsPreferenceItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.encrypt_local_files), chatModel.controller.appPrefs.privacyEncryptLocalFiles, onChange = { enable ->
withBGApi { chatModel.controller.apiSetEncryptLocalFiles(enable) }
})
SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages)
SettingsPreferenceItem(painterResource(MR.images.ic_security), stringResource(MR.strings.protect_ip_address), chatModel.controller.appPrefs.privacyAskToApproveRelays)
}
SectionCustomFooter {
if (chatModel.controller.appPrefs.privacyAskToApproveRelays.state.value) {
Text(stringResource(MR.strings.app_will_ask_to_confirm_unknown_file_servers))
} else {
Text(stringResource(MR.strings.without_tor_or_vpn_ip_address_will_be_visible_to_file_servers))
}
}
val currentUser = chatModel.currentUser.value val currentUser = chatModel.currentUser.value
if (currentUser != null) { if (currentUser != null) {
@@ -141,7 +154,7 @@ fun PrivacySettingsView(
} }
if (!chatModel.desktopNoUserNoRemote) { if (!chatModel.desktopNoUserNoRemote) {
SectionDividerSpaced() SectionDividerSpaced(maxTopPadding = true)
DeliveryReceiptsSection( DeliveryReceiptsSection(
currentUser = currentUser, currentUser = currentUser,
setOrAskSendReceiptsContacts = { enable -> setOrAskSendReceiptsContacts = { enable ->
@@ -119,6 +119,8 @@
<string name="error_joining_group">Error joining group</string> <string name="error_joining_group">Error joining group</string>
<string name="cannot_receive_file">Cannot receive file</string> <string name="cannot_receive_file">Cannot receive file</string>
<string name="sender_cancelled_file_transfer">Sender cancelled file transfer.</string> <string name="sender_cancelled_file_transfer">Sender cancelled file transfer.</string>
<string name="file_not_approved_title">Unknown servers!</string>
<string name="file_not_approved_descr">Without Tor or VPN, your IP address will be visible to these XFTP relays:\n%1$s.</string>
<string name="error_receiving_file">Error receiving file</string> <string name="error_receiving_file">Error receiving file</string>
<string name="error_creating_address">Error creating address</string> <string name="error_creating_address">Error creating address</string>
<string name="contact_already_exists">Contact already exists</string> <string name="contact_already_exists">Contact already exists</string>
@@ -992,6 +994,9 @@
<string name="protect_app_screen">Protect app screen</string> <string name="protect_app_screen">Protect app screen</string>
<string name="encrypt_local_files">Encrypt local files</string> <string name="encrypt_local_files">Encrypt local files</string>
<string name="auto_accept_images">Auto-accept images</string> <string name="auto_accept_images">Auto-accept images</string>
<string name="protect_ip_address">Protect IP address</string>
<string name="app_will_ask_to_confirm_unknown_file_servers">The app will ask to confirm downloads from unknown file servers (except .onion or when SOCKS proxy is enabled).</string>
<string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Without Tor or VPN, your IP address will be visible to file servers.</string>
<string name="send_link_previews">Send link previews</string> <string name="send_link_previews">Send link previews</string>
<string name="privacy_show_last_messages">Show last messages</string> <string name="privacy_show_last_messages">Show last messages</string>
<string name="privacy_message_draft">Message draft</string> <string name="privacy_message_draft">Message draft</string>
@@ -1056,6 +1061,7 @@
<string name="settings_section_title_app">APP</string> <string name="settings_section_title_app">APP</string>
<string name="settings_section_title_device">DEVICE</string> <string name="settings_section_title_device">DEVICE</string>
<string name="settings_section_title_chats">CHATS</string> <string name="settings_section_title_chats">CHATS</string>
<string name="settings_section_title_files">FILES</string>
<string name="settings_section_title_delivery_receipts">SEND DELIVERY RECEIPTS TO</string> <string name="settings_section_title_delivery_receipts">SEND DELIVERY RECEIPTS TO</string>
<string name="settings_restart_app">Restart</string> <string name="settings_restart_app">Restart</string>
<string name="settings_shutdown">Shutdown</string> <string name="settings_shutdown">Shutdown</string>
+4 -4
View File
@@ -26,11 +26,11 @@ android.enableJetifier=true
kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.jvm.target=11 kotlin.jvm.target=11
android.version_name=5.8-beta.1 android.version_name=5.8-beta.2
android.version_code=209 android.version_code=210
desktop.version_name=5.8-beta.1 desktop.version_name=5.8-beta.2
desktop.version_code=46 desktop.version_code=47
kotlin.version=1.9.23 kotlin.version=1.9.23
gradle.plugin.version=8.2.0 gradle.plugin.version=8.2.0
@@ -31,7 +31,7 @@ import Directory.Search
import Directory.Store import Directory.Store
import Simplex.Chat.Bot import Simplex.Chat.Bot
import Simplex.Chat.Bot.KnownContacts import Simplex.Chat.Bot.KnownContacts
import Simplex.Chat.Controller hiding (logError, logInfo) import Simplex.Chat.Controller
import Simplex.Chat.Core import Simplex.Chat.Core
import Simplex.Chat.Messages import Simplex.Chat.Messages
import Simplex.Chat.Options import Simplex.Chat.Options
@@ -586,8 +586,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
sendChatCmdStr cc cmdStr >>= \r -> do sendChatCmdStr cc cmdStr >>= \r -> do
ts <- getCurrentTime ts <- getCurrentTime
tz <- getCurrentTimeZone tz <- getCurrentTimeZone
ll <- readTVarIO $ appLogLevel cc sendReply $ serializeChatResponse (Nothing, Just user) ts tz Nothing r
sendReply $ serializeChatResponse (Nothing, Just user) ll ts tz Nothing r
DCCommandError tag -> sendReply $ "Command error: " <> show tag DCCommandError tag -> sendReply $ "Command error: " <> show tag
| otherwise = sendReply "You are not allowed to use this command" | otherwise = sendReply "You are not allowed to use this command"
where where
+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: 71489fe6fca70f32f18137186bab2b77304f11b0 tag: e3f5d244c1a435593e33adc023bf1f920f379f8d
source-repository-package source-repository-package
type: git type: git
+1 -1
View File
@@ -1,5 +1,5 @@
name: simplex-chat name: simplex-chat
version: 5.8.0.1 version: 5.8.0.2
#synopsis: #synopsis:
#description: #description:
homepage: https://github.com/simplex-chat/simplex-chat#readme homepage: https://github.com/simplex-chat/simplex-chat#readme
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"https://github.com/simplex-chat/simplexmq.git"."71489fe6fca70f32f18137186bab2b77304f11b0" = "18inrqiab269w7dw1isjrijgnycv0dz0bp1y9sq5z9fykvwg5rlh"; "https://github.com/simplex-chat/simplexmq.git"."e3f5d244c1a435593e33adc023bf1f920f379f8d" = "1klin78kgvgzdvf64nahn3280m7hw5f8wzrca43cmyajm2qp3wfs";
"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";
+2 -1
View File
@@ -5,7 +5,7 @@ cabal-version: 1.12
-- see: https://github.com/sol/hpack -- see: https://github.com/sol/hpack
name: simplex-chat name: simplex-chat
version: 5.8.0.1 version: 5.8.0.2
category: Web, System, Services, Cryptography category: Web, System, Services, Cryptography
homepage: https://github.com/simplex-chat/simplex-chat#readme homepage: https://github.com/simplex-chat/simplex-chat#readme
author: simplex.chat author: simplex.chat
@@ -144,6 +144,7 @@ library
Simplex.Chat.Migrations.M20240430_ui_theme Simplex.Chat.Migrations.M20240430_ui_theme
Simplex.Chat.Migrations.M20240501_chat_deleted Simplex.Chat.Migrations.M20240501_chat_deleted
Simplex.Chat.Migrations.M20240510_chat_items_via_proxy Simplex.Chat.Migrations.M20240510_chat_items_via_proxy
Simplex.Chat.Migrations.M20240515_rcv_files_user_approved_relays
Simplex.Chat.Mobile Simplex.Chat.Mobile
Simplex.Chat.Mobile.File Simplex.Chat.Mobile.File
Simplex.Chat.Mobile.Shared Simplex.Chat.Mobile.Shared
+92 -54
View File
@@ -17,7 +17,7 @@ module Simplex.Chat where
import Control.Applicative (optional, (<|>)) import Control.Applicative (optional, (<|>))
import Control.Concurrent.STM (retry) import Control.Concurrent.STM (retry)
import Control.Logger.Simple (LogConfig (..)) import Control.Logger.Simple
import Control.Monad import Control.Monad
import Control.Monad.Except import Control.Monad.Except
import Control.Monad.IO.Unlift import Control.Monad.IO.Unlift
@@ -47,6 +47,7 @@ import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M import qualified Data.Map.Strict as M
import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, listToMaybe, mapMaybe, maybeToList) import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, listToMaybe, mapMaybe, maybeToList)
import Data.Ord (Down (..)) import Data.Ord (Down (..))
import qualified Data.Set as S
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
import Data.Text.Encoding (decodeLatin1, encodeUtf8) import Data.Text.Encoding (decodeLatin1, encodeUtf8)
@@ -90,8 +91,9 @@ import Simplex.FileTransfer.Client.Presets (defaultXFTPServers)
import Simplex.FileTransfer.Description (FileDescriptionURI (..), ValidFileDescription) import Simplex.FileTransfer.Description (FileDescriptionURI (..), ValidFileDescription)
import qualified Simplex.FileTransfer.Description as FD import qualified Simplex.FileTransfer.Description as FD
import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI) import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI)
import qualified Simplex.FileTransfer.Transport as XFTP
import Simplex.Messaging.Agent as Agent import Simplex.Messaging.Agent as Agent
import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentClientStore, getAgentWorkersDetails, getAgentWorkersSummary, temporaryAgentError, withLockMap) import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentClientStore, getAgentWorkersDetails, getAgentWorkersSummary, ipAddressProtected, temporaryAgentError, withLockMap)
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig)
import Simplex.Messaging.Agent.Lock (withLock) import Simplex.Messaging.Agent.Lock (withLock)
import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Protocol
@@ -109,7 +111,7 @@ import qualified Simplex.Messaging.Crypto.Ratchet as CR
import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (base64P) import Simplex.Messaging.Parsers (base64P)
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), EntityId, ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolTypeI, SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol) import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), EntityId, ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth (..), ProtocolTypeI, SProtocolType (..), SubscriptionMode (..), UserProtocol, XFTPServer, userProtocol)
import qualified Simplex.Messaging.Protocol as SMP import qualified Simplex.Messaging.Protocol as SMP
import Simplex.Messaging.ServiceScheme (ServiceScheme (..)) import Simplex.Messaging.ServiceScheme (ServiceScheme (..))
import qualified Simplex.Messaging.TMap as TM import qualified Simplex.Messaging.TMap as TM
@@ -155,6 +157,7 @@ defaultChatConfig =
autoAcceptFileSize = 0, autoAcceptFileSize = 0,
showReactions = False, showReactions = False,
showReceipts = False, showReceipts = False,
logLevel = CLLImportant,
subscriptionEvents = False, subscriptionEvents = False,
hostEvents = False, hostEvents = False,
testView = False, testView = False,
@@ -217,7 +220,7 @@ newChatController
ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, networkConfig, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable}, deviceName, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize} ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, networkConfig, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable}, deviceName, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize}
backgroundMode = do backgroundMode = do
let inlineFiles' = if allowInstantFiles || autoAcceptFileSize > 0 then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False} let inlineFiles' = if allowInstantFiles || autoAcceptFileSize > 0 then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False}
config = cfg {showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, defaultServers = configServers, inlineFiles = inlineFiles', autoAcceptFileSize, highlyAvailable} config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, defaultServers = configServers, inlineFiles = inlineFiles', autoAcceptFileSize, highlyAvailable}
firstTime = dbNew chatStore firstTime = dbNew chatStore
currentUser <- newTVarIO user currentUser <- newTVarIO user
currentRemoteHost <- newTVarIO Nothing currentRemoteHost <- newTVarIO Nothing
@@ -240,7 +243,6 @@ newChatController
remoteHostSessions <- atomically TM.empty remoteHostSessions <- atomically TM.empty
remoteHostsFolder <- newTVarIO Nothing remoteHostsFolder <- newTVarIO Nothing
remoteCtrlSession <- newTVarIO Nothing remoteCtrlSession <- newTVarIO Nothing
appLogLevel <- newTVarIO logLevel
filesFolder <- newTVarIO optFilesFolder filesFolder <- newTVarIO optFilesFolder
chatStoreChanged <- newTVarIO False chatStoreChanged <- newTVarIO False
expireCIThreads <- newTVarIO M.empty expireCIThreads <- newTVarIO M.empty
@@ -279,7 +281,6 @@ newChatController
remoteHostsFolder, remoteHostsFolder,
remoteCtrlSession, remoteCtrlSession,
config, config,
appLogLevel,
filesFolder, filesFolder,
expireCIThreads, expireCIThreads,
expireCIFlags, expireCIFlags,
@@ -428,7 +429,7 @@ startReceiveUserFiles user = do
filesToReceive <- withStore' (`getRcvFilesToReceive` user) filesToReceive <- withStore' (`getRcvFilesToReceive` user)
forM_ filesToReceive $ \ft -> forM_ filesToReceive $ \ft ->
flip catchChatError (toView . CRChatError (Just user)) $ flip catchChatError (toView . CRChatError (Just user)) $
toView =<< receiveFile' user ft Nothing Nothing toView =<< receiveFile' user ft False Nothing Nothing
restoreCalls :: CM' () restoreCalls :: CM' ()
restoreCalls = do restoreCalls = do
@@ -437,19 +438,18 @@ restoreCalls = do
calls <- asks currentCalls calls <- asks currentCalls
atomically $ writeTVar calls callsMap atomically $ writeTVar calls callsMap
stopChatController :: ChatController -> CM' () stopChatController :: ChatController -> IO ()
stopChatController ChatController {smpAgent, agentAsync = s, sndFiles, rcvFiles, expireCIFlags, remoteHostSessions, remoteCtrlSession} = do stopChatController ChatController {smpAgent, agentAsync = s, sndFiles, rcvFiles, expireCIFlags, remoteHostSessions, remoteCtrlSession} = do
readTVarIO remoteHostSessions >>= mapM_ (cancelRemoteHost False . snd) readTVarIO remoteHostSessions >>= mapM_ (cancelRemoteHost False . snd)
liftIO $ do atomically (stateTVar remoteCtrlSession (,Nothing)) >>= mapM_ (cancelRemoteCtrl False . snd)
atomically (stateTVar remoteCtrlSession (,Nothing)) >>= mapM_ (cancelRemoteCtrl False . snd) disconnectAgentClient smpAgent
disconnectAgentClient smpAgent readTVarIO s >>= mapM_ (\(a1, a2) -> uninterruptibleCancel a1 >> mapM_ uninterruptibleCancel a2)
readTVarIO s >>= mapM_ (\(a1, a2) -> uninterruptibleCancel a1 >> mapM_ uninterruptibleCancel a2) closeFiles sndFiles
closeFiles sndFiles closeFiles rcvFiles
closeFiles rcvFiles atomically $ do
atomically $ do keys <- M.keys <$> readTVar expireCIFlags
keys <- M.keys <$> readTVar expireCIFlags forM_ keys $ \k -> TM.insert k False expireCIFlags
forM_ keys $ \k -> TM.insert k False expireCIFlags writeTVar s Nothing
writeTVar s Nothing
where where
closeFiles :: TVar (Map Int64 Handle) -> IO () closeFiles :: TVar (Map Int64 Handle) -> IO ()
closeFiles files = do closeFiles files = do
@@ -603,7 +603,7 @@ processChatCommand' vr = \case
Just _ -> pure CRChatRunning Just _ -> pure CRChatRunning
_ -> checkStoreNotChanged . lift $ startChatController mainApp $> CRChatStarted _ -> checkStoreNotChanged . lift $ startChatController mainApp $> CRChatStarted
APIStopChat -> do APIStopChat -> do
ask >>= lift . stopChatController ask >>= liftIO . stopChatController
pure CRChatStopped pure CRChatStopped
APIActivateChat restoreChat -> withUser $ \_ -> do APIActivateChat restoreChat -> withUser $ \_ -> do
lift $ when restoreChat restoreCalls lift $ when restoreChat restoreCalls
@@ -2057,17 +2057,17 @@ processChatCommand' vr = \case
ForwardFile chatName fileId -> forwardFile chatName fileId SendFile ForwardFile chatName fileId -> forwardFile chatName fileId SendFile
ForwardImage chatName fileId -> forwardFile chatName fileId SendImage ForwardImage chatName fileId -> forwardFile chatName fileId SendImage
SendFileDescription _chatName _f -> pure $ chatCmdError Nothing "TODO" SendFileDescription _chatName _f -> pure $ chatCmdError Nothing "TODO"
ReceiveFile fileId encrypted_ rcvInline_ filePath_ -> withUser $ \_ -> ReceiveFile fileId userApprovedRelays encrypted_ rcvInline_ filePath_ -> withUser $ \_ ->
withFileLock "receiveFile" fileId . procCmd $ do withFileLock "receiveFile" fileId . procCmd $ do
(user, ft@RcvFileTransfer {fileStatus}) <- withStore (`getRcvFileTransferById` fileId) (user, ft@RcvFileTransfer {fileStatus}) <- withStore (`getRcvFileTransferById` fileId)
encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles
ft' <- (if encrypt && fileStatus == RFSNew then setFileToEncrypt else pure) ft ft' <- (if encrypt && fileStatus == RFSNew then setFileToEncrypt else pure) ft
receiveFile' user ft' rcvInline_ filePath_ receiveFile' user ft' userApprovedRelays rcvInline_ filePath_
SetFileToReceive fileId encrypted_ -> withUser $ \_ -> do SetFileToReceive fileId userApprovedRelays encrypted_ -> withUser $ \_ -> do
withFileLock "setFileToReceive" fileId . procCmd $ do withFileLock "setFileToReceive" fileId . procCmd $ do
encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles
cfArgs <- if encrypt then Just <$> (atomically . CF.randomArgs =<< asks random) else pure Nothing cfArgs <- if encrypt then Just <$> (atomically . CF.randomArgs =<< asks random) else pure Nothing
withStore' $ \db -> setRcvFileToReceive db fileId cfArgs withStore' $ \db -> setRcvFileToReceive db fileId userApprovedRelays cfArgs
ok_ ok_
CancelFile fileId -> withUser $ \user@User {userId} -> CancelFile fileId -> withUser $ \user@User {userId} ->
withFileLock "cancelFile" fileId . procCmd $ withFileLock "cancelFile" fileId . procCmd $
@@ -2107,13 +2107,8 @@ processChatCommand' vr = \case
liftIO $ removeFile fsFilePath `catchAll_` pure () liftIO $ removeFile fsFilePath `catchAll_` pure ()
lift . forM_ agentRcvFileId $ \(AgentRcvFileId aFileId) -> lift . forM_ agentRcvFileId $ \(AgentRcvFileId aFileId) ->
withAgent' (`xftpDeleteRcvFile` aFileId) withAgent' (`xftpDeleteRcvFile` aFileId)
ci <- withStore $ \db -> do aci_ <- resetRcvCIFileStatus user fileId CIFSRcvInvitation
liftIO $ do pure $ CRRcvFileCancelled user aci_ ftr
updateCIFileStatus db user fileId CIFSRcvInvitation
updateRcvFileStatus db fileId FSNew
updateRcvFileAgentId db fileId Nothing
lookupChatItemByFileId db vr user fileId
pure $ CRRcvFileCancelled user ci ftr
FileStatus fileId -> withUser $ \user -> do FileStatus fileId -> withUser $ \user -> do
withStore (\db -> lookupChatItemByFileId db vr user fileId) >>= \case withStore (\db -> lookupChatItemByFileId db vr user fileId) >>= \case
Nothing -> do Nothing -> do
@@ -2201,10 +2196,6 @@ processChatCommand' vr = \case
chatMigrations <- map upMigration <$> withStore' (Migrations.getCurrent . DB.conn) chatMigrations <- map upMigration <$> withStore' (Migrations.getCurrent . DB.conn)
agentMigrations <- withAgent getAgentMigrations agentMigrations <- withAgent getAgentMigrations
pure $ CRVersionInfo {versionInfo, chatMigrations, agentMigrations} pure $ CRVersionInfo {versionInfo, chatMigrations, agentMigrations}
SetAppLogLevel ll -> do
chatWriteVar appLogLevel ll
lift $ withAgent' (`setAgentLogLevel` toLogLevel ll)
ok_
DebugLocks -> lift $ do DebugLocks -> lift $ do
chatLockName <- atomically . tryReadTMVar =<< asks chatLock chatLockName <- atomically . tryReadTMVar =<< asks chatLock
chatEntityLocks <- getLocks =<< asks entityLocks chatEntityLocks <- getLocks =<< asks entityLocks
@@ -3058,9 +3049,9 @@ setFileToEncrypt ft@RcvFileTransfer {fileId} = do
withStore' $ \db -> setFileCryptoArgs db fileId cfArgs withStore' $ \db -> setFileCryptoArgs db fileId cfArgs
pure (ft :: RcvFileTransfer) {cryptoArgs = Just cfArgs} pure (ft :: RcvFileTransfer) {cryptoArgs = Just cfArgs}
receiveFile' :: User -> RcvFileTransfer -> Maybe Bool -> Maybe FilePath -> CM ChatResponse receiveFile' :: User -> RcvFileTransfer -> Bool -> Maybe Bool -> Maybe FilePath -> CM ChatResponse
receiveFile' user ft rcvInline_ filePath_ = do receiveFile' user ft userApprovedRelays rcvInline_ filePath_ = do
(CRRcvFileAccepted user <$> acceptFileReceive user ft rcvInline_ filePath_) `catchChatError` processError (CRRcvFileAccepted user <$> acceptFileReceive user ft userApprovedRelays rcvInline_ filePath_) `catchChatError` processError
where where
processError = \case processError = \case
-- TODO AChatItem in Cancelled events -- TODO AChatItem in Cancelled events
@@ -3068,8 +3059,8 @@ receiveFile' user ft rcvInline_ filePath_ = do
ChatErrorAgent (CONN DUPLICATE) _ -> pure $ CRRcvFileAcceptedSndCancelled user ft ChatErrorAgent (CONN DUPLICATE) _ -> pure $ CRRcvFileAcceptedSndCancelled user ft
e -> throwError e e -> throwError e
acceptFileReceive :: User -> RcvFileTransfer -> Maybe Bool -> Maybe FilePath -> CM AChatItem acceptFileReceive :: User -> RcvFileTransfer -> Bool -> Maybe Bool -> Maybe FilePath -> CM AChatItem
acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = FileInvitation {fileName = fName, fileConnReq, fileInline, fileSize}, fileStatus, grpMemberId, cryptoArgs} rcvInline_ filePath_ = do acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = FileInvitation {fileName = fName, fileConnReq, fileInline, fileSize}, fileStatus, grpMemberId, cryptoArgs} userApprovedRelays rcvInline_ filePath_ = do
unless (fileStatus == RFSNew) $ case fileStatus of unless (fileStatus == RFSNew) $ case fileStatus of
RFSCancelled _ -> throwChatError $ CEFileCancelled fName RFSCancelled _ -> throwChatError $ CEFileCancelled fName
_ -> throwChatError $ CEFileAlreadyReceiving fName _ -> throwChatError $ CEFileAlreadyReceiving fName
@@ -3083,15 +3074,16 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI
filePath <- getRcvFilePath fileId filePath_ fName True filePath <- getRcvFilePath fileId filePath_ fName True
withStore $ \db -> acceptRcvFileTransfer db vr user fileId connIds ConnJoined filePath subMode withStore $ \db -> acceptRcvFileTransfer db vr user fileId connIds ConnJoined filePath subMode
-- XFTP -- XFTP
(Just XFTPRcvFile {}, _) -> do (Just XFTPRcvFile {userApprovedRelays = approvedBeforeReady}, _) -> do
let userApproved = approvedBeforeReady || userApprovedRelays
filePath <- getRcvFilePath fileId filePath_ fName False filePath <- getRcvFilePath fileId filePath_ fName False
(ci, rfd) <- withStore $ \db -> do (ci, rfd) <- withStore $ \db -> do
-- marking file as accepted and reading description in the same transaction -- marking file as accepted and reading description in the same transaction
-- to prevent race condition with appending description -- to prevent race condition with appending description
ci <- xftpAcceptRcvFT db vr user fileId filePath ci <- xftpAcceptRcvFT db vr user fileId filePath userApproved
rfd <- getRcvFileDescrByRcvFileId db fileId rfd <- getRcvFileDescrByRcvFileId db fileId
pure (ci, rfd) pure (ci, rfd)
receiveViaCompleteFD user fileId rfd cryptoArgs receiveViaCompleteFD user fileId rfd userApproved cryptoArgs
pure ci pure ci
-- group & direct file protocol -- group & direct file protocol
_ -> do _ -> do
@@ -3136,18 +3128,61 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI
|| (rcvInline_ == Just True && fileSize <= fileChunkSize * offerChunks) || (rcvInline_ == Just True && fileSize <= fileChunkSize * offerChunks)
) )
receiveViaCompleteFD :: User -> FileTransferId -> RcvFileDescr -> Maybe CryptoFileArgs -> CM () receiveViaCompleteFD :: User -> FileTransferId -> RcvFileDescr -> Bool -> Maybe CryptoFileArgs -> CM ()
receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} cfArgs = receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} userApprovedRelays cfArgs =
when fileDescrComplete $ do when fileDescrComplete $ do
rd <- parseFileDescription fileDescrText rd <- parseFileDescription fileDescrText
aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) rd cfArgs if userApprovedRelays
startReceivingFile user fileId then receive' rd True
withStore' $ \db -> updateRcvFileAgentId db fileId (Just $ AgentRcvFileId aFileId) else do
let srvs = fileServers rd
unknownSrvs <- getUnknownSrvs srvs
let approved = null unknownSrvs
ifM
((approved ||) <$> ipProtectedForSrvs srvs)
(receive' rd approved)
(relaysNotApproved unknownSrvs)
where
receive' :: ValidFileDescription 'FRecipient -> Bool -> CM ()
receive' rd approved = do
aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) rd cfArgs approved
startReceivingFile user fileId
withStore' $ \db -> updateRcvFileAgentId db fileId (Just $ AgentRcvFileId aFileId)
fileServers :: ValidFileDescription 'FRecipient -> [XFTPServer]
fileServers (FD.ValidFileDescription FD.FileDescription {chunks}) =
S.toList $ S.fromList $ concatMap (\FD.FileChunk {replicas} -> map (\FD.FileChunkReplica {server} -> server) replicas) chunks
getUnknownSrvs :: [XFTPServer] -> CM [XFTPServer]
getUnknownSrvs srvs = do
ChatConfig {defaultServers = DefaultAgentServers {xftp = defXftp}} <- asks config
storedSrvs <- map (\ServerCfg {server} -> protoServer server) <$> withStore' (`getProtocolServers` user)
let defXftp' = L.map protoServer defXftp
knownSrvs = fromMaybe defXftp' $ nonEmpty storedSrvs
pure $ filter (`notElem` knownSrvs) srvs
ipProtectedForSrvs :: [XFTPServer] -> CM Bool
ipProtectedForSrvs srvs = do
netCfg <- lift $ withAgent' getNetworkConfig
pure $ all (ipAddressProtected netCfg) srvs
relaysNotApproved :: [XFTPServer] -> CM ()
relaysNotApproved unknownSrvs = do
aci_ <- resetRcvCIFileStatus user fileId CIFSRcvInvitation
forM_ aci_ $ \aci -> toView $ CRChatItemUpdated user aci
throwChatError $ CEFileNotApproved fileId unknownSrvs
resetRcvCIFileStatus :: User -> FileTransferId -> CIFileStatus 'MDRcv -> CM (Maybe AChatItem)
resetRcvCIFileStatus user fileId ciFileStatus = do
vr <- chatVersionRange
withStore $ \db -> do
liftIO $ do
updateCIFileStatus db user fileId ciFileStatus
updateRcvFileStatus db fileId FSNew
updateRcvFileAgentId db fileId Nothing
lookupChatItemByFileId db vr user fileId
receiveViaURI :: User -> FileDescriptionURI -> CryptoFile -> CM RcvFileTransfer receiveViaURI :: User -> FileDescriptionURI -> CryptoFile -> CM RcvFileTransfer
receiveViaURI user@User {userId} FileDescriptionURI {description} cf@CryptoFile {cryptoArgs} = do receiveViaURI user@User {userId} FileDescriptionURI {description} cf@CryptoFile {cryptoArgs} = do
fileId <- withStore $ \db -> createRcvStandaloneFileTransfer db userId cf fileSize chunkSize fileId <- withStore $ \db -> createRcvStandaloneFileTransfer db userId cf fileSize chunkSize
aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) description cryptoArgs -- currently the only use case is user migrating via their configured servers, so we pass approvedRelays = True
aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) description cryptoArgs True
withStore $ \db -> do withStore $ \db -> do
liftIO $ do liftIO $ do
updateRcvFileStatus db fileId FSConnected updateRcvFileStatus db fileId FSConnected
@@ -3635,7 +3670,6 @@ processAgentMessageNoConn = \case
UP srv conns -> serverEvent srv conns NSConnected CRContactsSubscribed UP srv conns -> serverEvent srv conns NSConnected CRContactsSubscribed
SUSPENDED -> toView CRChatSuspended SUSPENDED -> toView CRChatSuspended
DEL_USER agentUserId -> toView $ CRAgentUserDeleted agentUserId DEL_USER agentUserId -> toView $ CRAgentUserDeleted agentUserId
LOG ll s -> toView $ CRAgentLog ll s
where where
hostEvent :: ChatResponse -> CM () hostEvent :: ChatResponse -> CM ()
hostEvent = whenM (asks $ hostEvents . config) . toView hostEvent = whenM (asks $ hostEvents . config) . toView
@@ -3818,6 +3852,10 @@ processAgentMsgRcvFile _corrId aFileId msg = do
RFERR e RFERR e
| temporaryAgentError e -> | temporaryAgentError e ->
throwChatError $ CEXFTPRcvFile fileId (AgentRcvFileId aFileId) e throwChatError $ CEXFTPRcvFile fileId (AgentRcvFileId aFileId) e
| e == XFTP "" XFTP.NOT_APPROVED -> do
aci_ <- resetRcvCIFileStatus user fileId CIFSRcvAborted
agentXFTPDeleteRcvFile aFileId fileId
forM_ aci_ $ \aci -> toView $ CRChatItemUpdated user aci
| otherwise -> do | otherwise -> do
ci <- withStore $ \db -> do ci <- withStore $ \db -> do
liftIO $ updateFileCancelled db user fileId CIFSRcvError liftIO $ updateFileCancelled db user fileId CIFSRcvError
@@ -4869,8 +4907,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
autoAcceptFile :: Maybe (RcvFileTransfer, CIFile 'MDRcv) -> CM () autoAcceptFile :: Maybe (RcvFileTransfer, CIFile 'MDRcv) -> CM ()
autoAcceptFile = mapM_ $ \(ft, CIFile {fileSize}) -> do autoAcceptFile = mapM_ $ \(ft, CIFile {fileSize}) -> do
-- ! autoAcceptFileSize is only used in tests
ChatConfig {autoAcceptFileSize = sz} <- asks config ChatConfig {autoAcceptFileSize = sz} <- asks config
when (sz > fileSize) $ receiveFile' user ft Nothing Nothing >>= toView when (sz > fileSize) $ receiveFile' user ft False Nothing Nothing >>= toView
messageFileDescription :: Contact -> SharedMsgId -> FileDescr -> CM () messageFileDescription :: Contact -> SharedMsgId -> FileDescr -> CM ()
messageFileDescription ct@Contact {contactId} sharedMsgId fileDescr = do messageFileDescription ct@Contact {contactId} sharedMsgId fileDescr = do
@@ -4896,7 +4935,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
ci <- withStore $ \db -> getAChatItemBySharedMsgId db user cd sharedMsgId ci <- withStore $ \db -> getAChatItemBySharedMsgId db user cd sharedMsgId
toView $ CRRcvFileDescrReady user ci ft' rfd toView $ CRRcvFileDescrReady user ci ft' rfd
case (fileStatus, xftpRcvFile) of case (fileStatus, xftpRcvFile) of
(RFSAccepted _, Just XFTPRcvFile {}) -> receiveViaCompleteFD user fileId rfd cryptoArgs (RFSAccepted _, Just XFTPRcvFile {userApprovedRelays}) -> receiveViaCompleteFD user fileId rfd userApprovedRelays cryptoArgs
_ -> pure () _ -> pure ()
processFileInvitation :: Maybe FileInvitation -> MsgContent -> (DB.Connection -> FileInvitation -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer) -> CM (Maybe (RcvFileTransfer, CIFile 'MDRcv)) processFileInvitation :: Maybe FileInvitation -> MsgContent -> (DB.Connection -> FileInvitation -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer) -> CM (Maybe (RcvFileTransfer, CIFile 'MDRcv))
@@ -7322,8 +7361,8 @@ chatCommandP =
("/fforward " <|> "/ff ") *> (ForwardFile <$> chatNameP' <* A.space <*> A.decimal), ("/fforward " <|> "/ff ") *> (ForwardFile <$> chatNameP' <* A.space <*> A.decimal),
("/image_forward " <|> "/imgf ") *> (ForwardImage <$> chatNameP' <* A.space <*> A.decimal), ("/image_forward " <|> "/imgf ") *> (ForwardImage <$> chatNameP' <* A.space <*> A.decimal),
("/fdescription " <|> "/fd") *> (SendFileDescription <$> chatNameP' <* A.space <*> filePath), ("/fdescription " <|> "/fd") *> (SendFileDescription <$> chatNameP' <* A.space <*> filePath),
("/freceive " <|> "/fr ") *> (ReceiveFile <$> A.decimal <*> optional (" encrypt=" *> onOffP) <*> optional (" inline=" *> onOffP) <*> optional (A.space *> filePath)), ("/freceive " <|> "/fr ") *> (ReceiveFile <$> A.decimal <*> (" approved_relays=" *> onOffP <|> pure False) <*> optional (" encrypt=" *> onOffP) <*> optional (" inline=" *> onOffP) <*> optional (A.space *> filePath)),
"/_set_file_to_receive " *> (SetFileToReceive <$> A.decimal <*> optional (" encrypt=" *> onOffP)), "/_set_file_to_receive " *> (SetFileToReceive <$> A.decimal <*> (" approved_relays=" *> onOffP <|> pure False) <*> optional (" encrypt=" *> onOffP)),
("/fcancel " <|> "/fc ") *> (CancelFile <$> A.decimal), ("/fcancel " <|> "/fc ") *> (CancelFile <$> A.decimal),
("/fstatus " <|> "/fs ") *> (FileStatus <$> A.decimal), ("/fstatus " <|> "/fs ") *> (FileStatus <$> A.decimal),
"/_connect contact " *> (APIConnectContactViaAddress <$> A.decimal <*> incognitoOnOffP <* A.space <*> A.decimal), "/_connect contact " *> (APIConnectContactViaAddress <$> A.decimal <*> incognitoOnOffP <* A.space <*> A.decimal),
@@ -7384,7 +7423,6 @@ chatCommandP =
"/_download " *> (APIDownloadStandaloneFile <$> A.decimal <* A.space <*> strP_ <*> cryptoFileP), "/_download " *> (APIDownloadStandaloneFile <$> A.decimal <* A.space <*> strP_ <*> cryptoFileP),
("/quit" <|> "/q" <|> "/exit") $> QuitChat, ("/quit" <|> "/q" <|> "/exit") $> QuitChat,
("/version" <|> "/v") $> ShowVersion, ("/version" <|> "/v") $> ShowVersion,
"/log " *> (SetAppLogLevel <$> strP),
"/debug locks" $> DebugLocks, "/debug locks" $> DebugLocks,
"/debug event " *> (DebugEvent <$> jsonP), "/debug event " *> (DebugEvent <$> jsonP),
"/get stats" $> GetAgentStats, "/get stats" $> GetAgentStats,
+6
View File
@@ -29,6 +29,7 @@ data AppSettings = AppSettings
{ appPlatform :: Maybe AppPlatform, { appPlatform :: Maybe AppPlatform,
networkConfig :: Maybe NetworkConfig, networkConfig :: Maybe NetworkConfig,
privacyEncryptLocalFiles :: Maybe Bool, privacyEncryptLocalFiles :: Maybe Bool,
privacyAskToApproveRelays :: Maybe Bool,
privacyAcceptImages :: Maybe Bool, privacyAcceptImages :: Maybe Bool,
privacyLinkPreviews :: Maybe Bool, privacyLinkPreviews :: Maybe Bool,
privacyShowChatPreviews :: Maybe Bool, privacyShowChatPreviews :: Maybe Bool,
@@ -61,6 +62,7 @@ defaultAppSettings =
{ appPlatform = Nothing, { appPlatform = Nothing,
networkConfig = Just defaultNetworkConfig, networkConfig = Just defaultNetworkConfig,
privacyEncryptLocalFiles = Just True, privacyEncryptLocalFiles = Just True,
privacyAskToApproveRelays = Just True,
privacyAcceptImages = Just True, privacyAcceptImages = Just True,
privacyLinkPreviews = Just True, privacyLinkPreviews = Just True,
privacyShowChatPreviews = Just True, privacyShowChatPreviews = Just True,
@@ -92,6 +94,7 @@ defaultParseAppSettings =
{ appPlatform = Nothing, { appPlatform = Nothing,
networkConfig = Nothing, networkConfig = Nothing,
privacyEncryptLocalFiles = Nothing, privacyEncryptLocalFiles = Nothing,
privacyAskToApproveRelays = Nothing,
privacyAcceptImages = Nothing, privacyAcceptImages = Nothing,
privacyLinkPreviews = Nothing, privacyLinkPreviews = Nothing,
privacyShowChatPreviews = Nothing, privacyShowChatPreviews = Nothing,
@@ -123,6 +126,7 @@ combineAppSettings platformDefaults storedSettings =
{ appPlatform = p appPlatform, { appPlatform = p appPlatform,
networkConfig = p networkConfig, networkConfig = p networkConfig,
privacyEncryptLocalFiles = p privacyEncryptLocalFiles, privacyEncryptLocalFiles = p privacyEncryptLocalFiles,
privacyAskToApproveRelays = p privacyAskToApproveRelays,
privacyAcceptImages = p privacyAcceptImages, privacyAcceptImages = p privacyAcceptImages,
privacyLinkPreviews = p privacyLinkPreviews, privacyLinkPreviews = p privacyLinkPreviews,
privacyShowChatPreviews = p privacyShowChatPreviews, privacyShowChatPreviews = p privacyShowChatPreviews,
@@ -166,6 +170,7 @@ instance FromJSON AppSettings where
appPlatform <- p "appPlatform" appPlatform <- p "appPlatform"
networkConfig <- p "networkConfig" networkConfig <- p "networkConfig"
privacyEncryptLocalFiles <- p "privacyEncryptLocalFiles" privacyEncryptLocalFiles <- p "privacyEncryptLocalFiles"
privacyAskToApproveRelays <- p "privacyAskToApproveRelays"
privacyAcceptImages <- p "privacyAcceptImages" privacyAcceptImages <- p "privacyAcceptImages"
privacyLinkPreviews <- p "privacyLinkPreviews" privacyLinkPreviews <- p "privacyLinkPreviews"
privacyShowChatPreviews <- p "privacyShowChatPreviews" privacyShowChatPreviews <- p "privacyShowChatPreviews"
@@ -194,6 +199,7 @@ instance FromJSON AppSettings where
{ appPlatform, { appPlatform,
networkConfig, networkConfig,
privacyEncryptLocalFiles, privacyEncryptLocalFiles,
privacyAskToApproveRelays,
privacyAcceptImages, privacyAcceptImages,
privacyLinkPreviews, privacyLinkPreviews,
privacyShowChatPreviews, privacyShowChatPreviews,
+3 -3
View File
@@ -85,9 +85,9 @@ textMsgContent :: String -> MsgContent
textMsgContent = MCText . T.pack textMsgContent = MCText . T.pack
printLog :: ChatController -> ChatLogLevel -> String -> IO () printLog :: ChatController -> ChatLogLevel -> String -> IO ()
printLog cc level s = do printLog cc level s
ll <- readTVarIO $ appLogLevel cc | logLevel (config cc) <= level = putStrLn s
when (ll <= level) $ putStrLn s | otherwise = pure ()
contactInfo :: Contact -> String contactInfo :: Contact -> String
contactInfo Contact {contactId, localDisplayName} = T.unpack localDisplayName <> " (" <> show contactId <> ")" contactInfo Contact {contactId, localDisplayName} = T.unpack localDisplayName <> " (" <> show contactId <> ")"
+5 -61
View File
@@ -19,8 +19,6 @@ module Simplex.Chat.Controller where
import Control.Concurrent (ThreadId) import Control.Concurrent (ThreadId)
import Control.Concurrent.Async (Async) import Control.Concurrent.Async (Async)
import Control.Exception import Control.Exception
import qualified Control.Logger.Simple as Logger
import Control.Monad (when)
import Control.Monad.Except import Control.Monad.Except
import Control.Monad.IO.Unlift import Control.Monad.IO.Unlift
import Control.Monad.Reader import Control.Monad.Reader
@@ -83,7 +81,7 @@ import Simplex.Messaging.Crypto.Ratchet (PQEncryption)
import Simplex.Messaging.Encoding.String import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus) import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus)
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON) import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON)
import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), CorrId, NtfServer, ProtoServerWithAuth, ProtocolTypeI, QueueId, SMPMsgMeta (..), SProtocolType, SubscriptionMode (..), UserProtocol, XFTPServerWithAuth, userProtocol) import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), CorrId, NtfServer, ProtoServerWithAuth, ProtocolTypeI, QueueId, SMPMsgMeta (..), SProtocolType, SubscriptionMode (..), UserProtocol, XFTPServer, XFTPServerWithAuth, userProtocol)
import Simplex.Messaging.TMap (TMap) import Simplex.Messaging.TMap (TMap)
import Simplex.Messaging.Transport (TLS, simplexMQVersion) import Simplex.Messaging.Transport (TLS, simplexMQVersion)
import Simplex.Messaging.Transport.Client (TransportHost) import Simplex.Messaging.Transport.Client (TransportHost)
@@ -141,6 +139,7 @@ data ChatConfig = ChatConfig
showReceipts :: Bool, showReceipts :: Bool,
subscriptionEvents :: Bool, subscriptionEvents :: Bool,
hostEvents :: Bool, hostEvents :: Bool,
logLevel :: ChatLogLevel,
testView :: Bool, testView :: Bool,
initialCleanupManagerDelay :: Int64, initialCleanupManagerDelay :: Int64,
cleanupManagerInterval :: NominalDiffTime, cleanupManagerInterval :: NominalDiffTime,
@@ -222,7 +221,6 @@ data ChatController = ChatController
remoteHostsFolder :: TVar (Maybe FilePath), -- folder for remote hosts data remoteHostsFolder :: TVar (Maybe FilePath), -- folder for remote hosts data
remoteCtrlSession :: TVar (Maybe (SessionSeq, RemoteCtrlSession)), -- Supervisor process for hosted controllers remoteCtrlSession :: TVar (Maybe (SessionSeq, RemoteCtrlSession)), -- Supervisor process for hosted controllers
config :: ChatConfig, config :: ChatConfig,
appLogLevel :: TVar ChatLogLevel,
filesFolder :: TVar (Maybe FilePath), -- path to files folder for mobile apps, filesFolder :: TVar (Maybe FilePath), -- path to files folder for mobile apps,
expireCIThreads :: TMap UserId (Maybe (Async ())), expireCIThreads :: TMap UserId (Maybe (Async ())),
expireCIFlags :: TMap UserId Bool, expireCIFlags :: TMap UserId Bool,
@@ -460,8 +458,8 @@ data ChatCommand
| ForwardFile ChatName FileTransferId | ForwardFile ChatName FileTransferId
| ForwardImage ChatName FileTransferId | ForwardImage ChatName FileTransferId
| SendFileDescription ChatName FilePath | SendFileDescription ChatName FilePath
| ReceiveFile {fileId :: FileTransferId, storeEncrypted :: Maybe Bool, fileInline :: Maybe Bool, filePath :: Maybe FilePath} | ReceiveFile {fileId :: FileTransferId, userApprovedRelays :: Bool, storeEncrypted :: Maybe Bool, fileInline :: Maybe Bool, filePath :: Maybe FilePath}
| SetFileToReceive {fileId :: FileTransferId, storeEncrypted :: Maybe Bool} | SetFileToReceive {fileId :: FileTransferId, userApprovedRelays :: Bool, storeEncrypted :: Maybe Bool}
| CancelFile FileTransferId | CancelFile FileTransferId
| FileStatus FileTransferId | FileStatus FileTransferId
| ShowProfile -- UserId (not used in UI) | ShowProfile -- UserId (not used in UI)
@@ -495,7 +493,6 @@ data ChatCommand
| APIStandaloneFileInfo FileDescriptionURI | APIStandaloneFileInfo FileDescriptionURI
| QuitChat | QuitChat
| ShowVersion | ShowVersion
| SetAppLogLevel ChatLogLevel
| DebugLocks | DebugLocks
| DebugEvent ChatResponse | DebugEvent ChatResponse
| GetAgentStats | GetAgentStats
@@ -760,8 +757,6 @@ data ChatResponse
| CRChatCmdError {user_ :: Maybe User, chatError :: ChatError} | CRChatCmdError {user_ :: Maybe User, chatError :: ChatError}
| CRChatError {user_ :: Maybe User, chatError :: ChatError} | CRChatError {user_ :: Maybe User, chatError :: ChatError}
| CRChatErrors {user_ :: Maybe User, chatErrors :: [ChatError]} | CRChatErrors {user_ :: Maybe User, chatErrors :: [ChatError]}
| CRAgentLog {agentLogLevel :: AgentLogLevel, errorMessage :: Text}
| CRChatLog {chatLogLevel :: ChatLogLevel, errorMessage :: Text}
| CRArchiveImported {archiveErrors :: [ArchiveError]} | CRArchiveImported {archiveErrors :: [ArchiveError]}
| CRAppSettings {appSettings :: AppSettings} | CRAppSettings {appSettings :: AppSettings}
| CRTimedAction {action :: String, durationMilliseconds :: Int64} | CRTimedAction {action :: String, durationMilliseconds :: Int64}
@@ -1058,30 +1053,6 @@ tmeToPref currentTTL tme = uncurry TimedMessagesPreference $ case tme of
data ChatLogLevel = CLLDebug | CLLInfo | CLLWarning | CLLError | CLLImportant data ChatLogLevel = CLLDebug | CLLInfo | CLLWarning | CLLError | CLLImportant
deriving (Eq, Ord, Show) deriving (Eq, Ord, Show)
instance StrEncoding ChatLogLevel where
strEncode = \case
CLLDebug -> "debug"
CLLInfo -> "info"
CLLWarning -> "warn"
CLLError -> "error"
CLLImportant -> "important"
strP =
A.takeTill (== ' ')
>>= \case
"debug" -> pure CLLDebug
"info" -> pure CLLInfo
"warn" -> pure CLLWarning
"error" -> pure CLLError
"important" -> pure CLLImportant
_ -> fail "Invalid log level"
instance ToJSON ChatLogLevel where
toJSON = strToJSON
toEncoding = strToJEncoding
instance FromJSON ChatLogLevel where
parseJSON = strParseJSON "ChatLogLevel"
data CoreVersionInfo = CoreVersionInfo data CoreVersionInfo = CoreVersionInfo
{ version :: String, { version :: String,
simplexmqVersion :: String, simplexmqVersion :: String,
@@ -1161,6 +1132,7 @@ data ChatErrorType
| CEFileImageType {filePath :: FilePath} | CEFileImageType {filePath :: FilePath}
| CEFileImageSize {filePath :: FilePath} | CEFileImageSize {filePath :: FilePath}
| CEFileNotReceived {fileId :: FileTransferId} | CEFileNotReceived {fileId :: FileTransferId}
| CEFileNotApproved {fileId :: FileTransferId, unknownServers :: [XFTPServer]}
| CEXFTPRcvFile {fileId :: FileTransferId, agentRcvFileId :: AgentRcvFileId, agentError :: AgentErrorType} | CEXFTPRcvFile {fileId :: FileTransferId, agentRcvFileId :: AgentRcvFileId, agentError :: AgentErrorType}
| CEXFTPSndFile {fileId :: FileTransferId, agentSndFileId :: AgentSndFileId, agentError :: AgentErrorType} | CEXFTPSndFile {fileId :: FileTransferId, agentSndFileId :: AgentSndFileId, agentError :: AgentErrorType}
| CEFallbackToSMPProhibited {fileId :: FileTransferId} | CEFallbackToSMPProhibited {fileId :: FileTransferId}
@@ -1426,34 +1398,6 @@ withAgent action =
withAgent' :: (AgentClient -> IO a) -> CM' a withAgent' :: (AgentClient -> IO a) -> CM' a
withAgent' action = asks smpAgent >>= liftIO . action withAgent' action = asks smpAgent >>= liftIO . action
logDebug :: Text -> CM ()
logDebug = lift . logDebug'
{-# INLINE logDebug #-}
logDebug' :: Text -> CM' ()
logDebug' s = logToView CLLDebug s >> Logger.logDebug s
logInfo :: Text -> CM ()
logInfo s = lift (logToView CLLInfo s) >> Logger.logInfo s
logWarn :: Text -> CM ()
logWarn s = lift (logToView CLLWarning s) >> Logger.logWarn s
logError :: Text -> CM ()
logError = lift . logError'
{-# INLINE logError #-}
logError' :: Text -> CM' ()
logError' s = logToView CLLError s >> Logger.logError s
logImportant :: Text -> CM ()
logImportant s = lift (logToView CLLImportant s) >> Logger.logError s
logToView :: ChatLogLevel -> Text -> CM' ()
logToView ll' s = do
ll <- chatReadVar' appLogLevel
when (ll' >= ll) $ toView' $ CRChatLog ll s
$(JQ.deriveJSON (enumJSON $ dropPrefix "HS") ''HelpSection) $(JQ.deriveJSON (enumJSON $ dropPrefix "HS") ''HelpSection)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CLQ") ''ChatListQuery) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CLQ") ''ChatListQuery)
+1 -3
View File
@@ -11,7 +11,6 @@ module Simplex.Chat.Core
) )
where where
import Control.Concurrent.STM
import Control.Logger.Simple import Control.Logger.Simple
import Control.Monad import Control.Monad
import Control.Monad.Reader import Control.Monad.Reader
@@ -111,8 +110,7 @@ createActiveUser cc = do
r -> do r -> do
ts <- getCurrentTime ts <- getCurrentTime
tz <- getCurrentTimeZone tz <- getCurrentTimeZone
ll <- readTVarIO $ appLogLevel cc putStrLn $ serializeChatResponse (Nothing, Nothing) ts tz Nothing r
putStrLn $ serializeChatResponse (Nothing, Nothing) ll ts tz Nothing r
loop loop
getWithPrompt :: String -> IO String getWithPrompt :: String -> IO String
+8
View File
@@ -539,6 +539,7 @@ data CIFileStatus (d :: MsgDirection) where
CIFSRcvInvitation :: CIFileStatus 'MDRcv CIFSRcvInvitation :: CIFileStatus 'MDRcv
CIFSRcvAccepted :: CIFileStatus 'MDRcv CIFSRcvAccepted :: CIFileStatus 'MDRcv
CIFSRcvTransfer :: {rcvProgress :: Int64, rcvTotal :: Int64} -> CIFileStatus 'MDRcv CIFSRcvTransfer :: {rcvProgress :: Int64, rcvTotal :: Int64} -> CIFileStatus 'MDRcv
CIFSRcvAborted :: CIFileStatus 'MDRcv
CIFSRcvComplete :: CIFileStatus 'MDRcv CIFSRcvComplete :: CIFileStatus 'MDRcv
CIFSRcvCancelled :: CIFileStatus 'MDRcv CIFSRcvCancelled :: CIFileStatus 'MDRcv
CIFSRcvError :: CIFileStatus 'MDRcv CIFSRcvError :: CIFileStatus 'MDRcv
@@ -558,6 +559,7 @@ ciFileEnded = \case
CIFSRcvInvitation -> False CIFSRcvInvitation -> False
CIFSRcvAccepted -> False CIFSRcvAccepted -> False
CIFSRcvTransfer {} -> False CIFSRcvTransfer {} -> False
CIFSRcvAborted -> True
CIFSRcvCancelled -> True CIFSRcvCancelled -> True
CIFSRcvComplete -> True CIFSRcvComplete -> True
CIFSRcvError -> True CIFSRcvError -> True
@@ -573,6 +575,7 @@ ciFileLoaded = \case
CIFSRcvInvitation -> False CIFSRcvInvitation -> False
CIFSRcvAccepted -> False CIFSRcvAccepted -> False
CIFSRcvTransfer {} -> False CIFSRcvTransfer {} -> False
CIFSRcvAborted -> False
CIFSRcvCancelled -> False CIFSRcvCancelled -> False
CIFSRcvComplete -> True CIFSRcvComplete -> True
CIFSRcvError -> False CIFSRcvError -> False
@@ -592,6 +595,7 @@ instance MsgDirectionI d => StrEncoding (CIFileStatus d) where
CIFSRcvInvitation -> "rcv_invitation" CIFSRcvInvitation -> "rcv_invitation"
CIFSRcvAccepted -> "rcv_accepted" CIFSRcvAccepted -> "rcv_accepted"
CIFSRcvTransfer rcvd total -> strEncode (Str "rcv_transfer", rcvd, total) CIFSRcvTransfer rcvd total -> strEncode (Str "rcv_transfer", rcvd, total)
CIFSRcvAborted -> "rcv_aborted"
CIFSRcvComplete -> "rcv_complete" CIFSRcvComplete -> "rcv_complete"
CIFSRcvCancelled -> "rcv_cancelled" CIFSRcvCancelled -> "rcv_cancelled"
CIFSRcvError -> "rcv_error" CIFSRcvError -> "rcv_error"
@@ -614,6 +618,7 @@ instance StrEncoding ACIFileStatus where
"rcv_invitation" -> pure $ AFS SMDRcv CIFSRcvInvitation "rcv_invitation" -> pure $ AFS SMDRcv CIFSRcvInvitation
"rcv_accepted" -> pure $ AFS SMDRcv CIFSRcvAccepted "rcv_accepted" -> pure $ AFS SMDRcv CIFSRcvAccepted
"rcv_transfer" -> AFS SMDRcv <$> progress CIFSRcvTransfer "rcv_transfer" -> AFS SMDRcv <$> progress CIFSRcvTransfer
"rcv_aborted" -> pure $ AFS SMDRcv CIFSRcvAborted
"rcv_complete" -> pure $ AFS SMDRcv CIFSRcvComplete "rcv_complete" -> pure $ AFS SMDRcv CIFSRcvComplete
"rcv_cancelled" -> pure $ AFS SMDRcv CIFSRcvCancelled "rcv_cancelled" -> pure $ AFS SMDRcv CIFSRcvCancelled
"rcv_error" -> pure $ AFS SMDRcv CIFSRcvError "rcv_error" -> pure $ AFS SMDRcv CIFSRcvError
@@ -631,6 +636,7 @@ data JSONCIFileStatus
| JCIFSRcvInvitation | JCIFSRcvInvitation
| JCIFSRcvAccepted | JCIFSRcvAccepted
| JCIFSRcvTransfer {rcvProgress :: Int64, rcvTotal :: Int64} | JCIFSRcvTransfer {rcvProgress :: Int64, rcvTotal :: Int64}
| JCIFSRcvAborted
| JCIFSRcvComplete | JCIFSRcvComplete
| JCIFSRcvCancelled | JCIFSRcvCancelled
| JCIFSRcvError | JCIFSRcvError
@@ -646,6 +652,7 @@ jsonCIFileStatus = \case
CIFSRcvInvitation -> JCIFSRcvInvitation CIFSRcvInvitation -> JCIFSRcvInvitation
CIFSRcvAccepted -> JCIFSRcvAccepted CIFSRcvAccepted -> JCIFSRcvAccepted
CIFSRcvTransfer rcvd total -> JCIFSRcvTransfer rcvd total CIFSRcvTransfer rcvd total -> JCIFSRcvTransfer rcvd total
CIFSRcvAborted -> JCIFSRcvAborted
CIFSRcvComplete -> JCIFSRcvComplete CIFSRcvComplete -> JCIFSRcvComplete
CIFSRcvCancelled -> JCIFSRcvCancelled CIFSRcvCancelled -> JCIFSRcvCancelled
CIFSRcvError -> JCIFSRcvError CIFSRcvError -> JCIFSRcvError
@@ -661,6 +668,7 @@ aciFileStatusJSON = \case
JCIFSRcvInvitation -> AFS SMDRcv CIFSRcvInvitation JCIFSRcvInvitation -> AFS SMDRcv CIFSRcvInvitation
JCIFSRcvAccepted -> AFS SMDRcv CIFSRcvAccepted JCIFSRcvAccepted -> AFS SMDRcv CIFSRcvAccepted
JCIFSRcvTransfer rcvd total -> AFS SMDRcv $ CIFSRcvTransfer rcvd total JCIFSRcvTransfer rcvd total -> AFS SMDRcv $ CIFSRcvTransfer rcvd total
JCIFSRcvAborted -> AFS SMDRcv CIFSRcvAborted
JCIFSRcvComplete -> AFS SMDRcv CIFSRcvComplete JCIFSRcvComplete -> AFS SMDRcv CIFSRcvComplete
JCIFSRcvCancelled -> AFS SMDRcv CIFSRcvCancelled JCIFSRcvCancelled -> AFS SMDRcv CIFSRcvCancelled
JCIFSRcvError -> AFS SMDRcv CIFSRcvError JCIFSRcvError -> AFS SMDRcv CIFSRcvError
@@ -0,0 +1,18 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Migrations.M20240515_rcv_files_user_approved_relays where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20240515_rcv_files_user_approved_relays :: Query
m20240515_rcv_files_user_approved_relays =
[sql|
ALTER TABLE rcv_files ADD COLUMN user_approved_relays INTEGER NOT NULL DEFAULT 0;
|]
down_m20240515_rcv_files_user_approved_relays :: Query
down_m20240515_rcv_files_user_approved_relays =
[sql|
ALTER TABLE rcv_files DROP COLUMN user_approved_relays;
|]
+2 -1
View File
@@ -229,7 +229,8 @@ CREATE TABLE rcv_files(
REFERENCES xftp_file_descriptions ON DELETE SET NULL, REFERENCES xftp_file_descriptions ON DELETE SET NULL,
agent_rcv_file_id BLOB NULL, agent_rcv_file_id BLOB NULL,
agent_rcv_file_deleted INTEGER DEFAULT 0 CHECK(agent_rcv_file_deleted NOT NULL), agent_rcv_file_deleted INTEGER DEFAULT 0 CHECK(agent_rcv_file_deleted NOT NULL),
to_receive INTEGER to_receive INTEGER,
user_approved_relays INTEGER NOT NULL DEFAULT 0
); );
CREATE TABLE snd_file_chunks( CREATE TABLE snd_file_chunks(
file_id INTEGER NOT NULL, file_id INTEGER NOT NULL,
+2 -1
View File
@@ -193,7 +193,7 @@ mobileChatOpts dbFilePrefix =
smpServers = [], smpServers = [],
xftpServers = [], xftpServers = [],
networkConfig = defaultNetworkConfig, networkConfig = defaultNetworkConfig,
logLevel = CLLError, logLevel = CLLImportant,
logConnections = False, logConnections = False,
logServerHosts = True, logServerHosts = True,
logAgent = Nothing, logAgent = Nothing,
@@ -220,6 +220,7 @@ defaultMobileConfig :: ChatConfig
defaultMobileConfig = defaultMobileConfig =
defaultChatConfig defaultChatConfig
{ confirmMigrations = MCYesUp, { confirmMigrations = MCYesUp,
logLevel = CLLError,
coreApi = True, coreApi = True,
deviceNameForRemote = "Mobile" deviceNameForRemote = "Mobile"
} }
+12 -7
View File
@@ -14,7 +14,6 @@ module Simplex.Chat.Options
getChatOpts, getChatOpts,
protocolServersP, protocolServersP,
fullNetworkConfig, fullNetworkConfig,
toLogLevel,
) )
where where
@@ -69,13 +68,13 @@ data CoreChatOpts = CoreChatOpts
data ChatCmdLog = CCLAll | CCLMessages | CCLNone data ChatCmdLog = CCLAll | CCLMessages | CCLNone
deriving (Eq) deriving (Eq)
toLogLevel :: ChatLogLevel -> LogLevel agentLogLevel :: ChatLogLevel -> LogLevel
toLogLevel = \case agentLogLevel = \case
CLLDebug -> LogDebug CLLDebug -> LogDebug
CLLInfo -> LogInfo CLLInfo -> LogInfo
CLLWarning -> LogWarn CLLWarning -> LogWarn
CLLError -> LogError CLLError -> LogError
CLLImportant -> LogError CLLImportant -> LogInfo
coreChatOptsP :: FilePath -> FilePath -> Parser CoreChatOpts coreChatOptsP :: FilePath -> FilePath -> Parser CoreChatOpts
coreChatOptsP appDir defaultDbFileName = do coreChatOptsP appDir defaultDbFileName = do
@@ -195,11 +194,11 @@ coreChatOptsP appDir defaultDbFileName = do
dbKey, dbKey,
smpServers, smpServers,
xftpServers, xftpServers,
networkConfig = fullNetworkConfig socksProxy (useTcpTimeout socksProxy t) (logTLSErrors || logLevel <= CLLDebug), networkConfig = fullNetworkConfig socksProxy (useTcpTimeout socksProxy t) (logTLSErrors || logLevel == CLLDebug),
logLevel, logLevel,
logConnections = logConnections || logLevel <= CLLInfo, logConnections = logConnections || logLevel <= CLLInfo,
logServerHosts = logServerHosts || logLevel <= CLLInfo, logServerHosts = logServerHosts || logLevel <= CLLInfo,
logAgent = if logAgent || logLevel <= CLLDebug then Just $ toLogLevel logLevel else Nothing, logAgent = if logAgent || logLevel == CLLDebug then Just $ agentLogLevel logLevel else Nothing,
logFile, logFile,
tbqSize, tbqSize,
highlyAvailable highlyAvailable
@@ -343,7 +342,13 @@ protocolServersP :: ProtocolTypeI p => A.Parser [ProtoServerWithAuth p]
protocolServersP = strP `A.sepBy1` A.char ' ' protocolServersP = strP `A.sepBy1` A.char ' '
parseLogLevel :: ReadM ChatLogLevel parseLogLevel :: ReadM ChatLogLevel
parseLogLevel = eitherReader $ strDecode . B.pack parseLogLevel = eitherReader $ \case
"debug" -> Right CLLDebug
"info" -> Right CLLInfo
"warn" -> Right CLLWarning
"error" -> Right CLLError
"important" -> Right CLLImportant
_ -> Left "Invalid log level"
parseChatCmdLog :: ReadM ChatCmdLog parseChatCmdLog :: ReadM ChatCmdLog
parseChatCmdLog = eitherReader $ \case parseChatCmdLog = eitherReader $ \case
+16 -16
View File
@@ -13,6 +13,7 @@
module Simplex.Chat.Remote where module Simplex.Chat.Remote where
import Control.Applicative ((<|>)) import Control.Applicative ((<|>))
import Control.Logger.Simple
import Control.Monad import Control.Monad
import Control.Monad.Except import Control.Monad.Except
import Control.Monad.IO.Class import Control.Monad.IO.Class
@@ -248,7 +249,7 @@ startRemoteHostSession rhKey = do
closeRemoteHost :: RHKey -> CM () closeRemoteHost :: RHKey -> CM ()
closeRemoteHost rhKey = do closeRemoteHost rhKey = do
logInfo $ "Closing remote host session for " <> tshow rhKey logNote $ "Closing remote host session for " <> tshow rhKey
cancelRemoteHostSession Nothing rhKey cancelRemoteHostSession Nothing rhKey
cancelRemoteHostSession :: Maybe (SessionSeq, RemoteHostStopReason) -> RHKey -> CM () cancelRemoteHostSession :: Maybe (SessionSeq, RemoteHostStopReason) -> RHKey -> CM ()
@@ -265,7 +266,7 @@ cancelRemoteHostSession handlerInfo_ rhKey = do
modifyTVar' crh $ \cur -> if (RHId <$> cur) == Just rhKey then Nothing else cur -- only wipe the closing RH modifyTVar' crh $ \cur -> if (RHId <$> cur) == Just rhKey then Nothing else cur -- only wipe the closing RH
pure $ Just rhs pure $ Just rhs
forM_ deregistered $ \session -> do forM_ deregistered $ \session -> do
lift (cancelRemoteHost handlingError session) `catchAny` (logError . tshow) liftIO $ cancelRemoteHost handlingError session `catchAny` (logError . tshow)
forM_ (snd <$> handlerInfo_) $ \rhStopReason -> forM_ (snd <$> handlerInfo_) $ \rhStopReason ->
toView CRRemoteHostStopped {remoteHostId_, rhsState = rhsSessionState session, rhStopReason} toView CRRemoteHostStopped {remoteHostId_, rhsState = rhsSessionState session, rhStopReason}
where where
@@ -274,26 +275,25 @@ cancelRemoteHostSession handlerInfo_ rhKey = do
RHNew -> Nothing RHNew -> Nothing
RHId rhId -> Just rhId RHId rhId -> Just rhId
cancelRemoteHost :: Bool -> RemoteHostSession -> CM' () cancelRemoteHost :: Bool -> RemoteHostSession -> IO ()
cancelRemoteHost handlingError = \case cancelRemoteHost handlingError = \case
RHSessionStarting -> pure () RHSessionStarting -> pure ()
RHSessionConnecting _inv rhs -> cancelPendingSession rhs RHSessionConnecting _inv rhs -> cancelPendingSession rhs
RHSessionPendingConfirmation _sessCode tls rhs -> do RHSessionPendingConfirmation _sessCode tls rhs -> do
cancelPendingSession rhs cancelPendingSession rhs
closeConn tls closeConnection tls
RHSessionConfirmed tls rhs -> do RHSessionConfirmed tls rhs -> do
cancelPendingSession rhs cancelPendingSession rhs
closeConn tls closeConnection tls
RHSessionConnected {rchClient, tls, rhClient = RemoteHostClient {httpClient}, pollAction} -> do RHSessionConnected {rchClient, tls, rhClient = RemoteHostClient {httpClient}, pollAction} -> do
uninterruptibleCancel pollAction uninterruptibleCancel pollAction
liftIO (cancelHostClient rchClient) `catchAny` (logError' . tshow) cancelHostClient rchClient `catchAny` (logError . tshow)
closeConn tls closeConnection tls `catchAny` (logError . tshow)
unless handlingError $ liftIO (closeHTTP2Client httpClient) `catchAny` (logError' . tshow) unless handlingError $ closeHTTP2Client httpClient `catchAny` (logError . tshow)
where where
closeConn tls = liftIO (closeConnection tls) `catchAny` (logError' . tshow)
cancelPendingSession RHPendingSession {rchClient, rhsWaitSession} = do cancelPendingSession RHPendingSession {rchClient, rhsWaitSession} = do
unless handlingError $ uninterruptibleCancel rhsWaitSession `catchAny` (logError' . tshow) unless handlingError $ uninterruptibleCancel rhsWaitSession `catchAny` (logError . tshow)
liftIO (cancelHostClient rchClient) `catchAny` (logError' . tshow) cancelHostClient rchClient `catchAny` (logError . tshow)
-- | Generate a random 16-char filepath without / in it by using base64url encoding. -- | Generate a random 16-char filepath without / in it by using base64url encoding.
randomStorePath :: IO FilePath randomStorePath :: IO FilePath
@@ -495,7 +495,7 @@ parseCtrlAppInfo ctrlAppInfo = do
handleRemoteCommand :: (ByteString -> CM' ChatResponse) -> RemoteCrypto -> TBQueue ChatResponse -> HTTP2Request -> CM' () handleRemoteCommand :: (ByteString -> CM' ChatResponse) -> RemoteCrypto -> TBQueue ChatResponse -> HTTP2Request -> CM' ()
handleRemoteCommand execChatCommand encryption remoteOutputQ HTTP2Request {request, reqBody, sendResponse} = do handleRemoteCommand execChatCommand encryption remoteOutputQ HTTP2Request {request, reqBody, sendResponse} = do
logDebug' "handleRemoteCommand" logDebug "handleRemoteCommand"
liftIO (tryRemoteError' parseRequest) >>= \case liftIO (tryRemoteError' parseRequest) >>= \case
Right (getNext, rc) -> do Right (getNext, rc) -> do
chatReadVar' currentUser >>= \case chatReadVar' currentUser >>= \case
@@ -511,7 +511,7 @@ handleRemoteCommand execChatCommand encryption remoteOutputQ HTTP2Request {reque
processCommand :: User -> GetChunk -> RemoteCommand -> CM () processCommand :: User -> GetChunk -> RemoteCommand -> CM ()
processCommand user getNext = \case processCommand user getNext = \case
RCSend {command} -> lift $ handleSend execChatCommand command >>= reply RCSend {command} -> lift $ handleSend execChatCommand command >>= reply
RCRecv {wait = time} -> lift $ handleRecv time remoteOutputQ >>= reply RCRecv {wait = time} -> lift $ liftIO (handleRecv time remoteOutputQ) >>= reply
RCStoreFile {fileName, fileSize, fileDigest} -> lift $ handleStoreFile encryption fileName fileSize fileDigest getNext >>= reply RCStoreFile {fileName, fileSize, fileDigest} -> lift $ handleStoreFile encryption fileName fileSize fileDigest getNext >>= reply
RCGetFile {file} -> handleGetFile encryption user file replyWith RCGetFile {file} -> handleGetFile encryption user file replyWith
reply :: RemoteResponse -> CM' () reply :: RemoteResponse -> CM' ()
@@ -547,14 +547,14 @@ tryRemoteError' = tryAllErrors' (RPEException . tshow)
handleSend :: (ByteString -> CM' ChatResponse) -> Text -> CM' RemoteResponse handleSend :: (ByteString -> CM' ChatResponse) -> Text -> CM' RemoteResponse
handleSend execChatCommand command = do handleSend execChatCommand command = do
logDebug' $ "Send: " <> tshow command logDebug $ "Send: " <> tshow command
-- execChatCommand checks for remote-allowed commands -- execChatCommand checks for remote-allowed commands
-- convert errors thrown in execChatCommand into error responses to prevent aborting the protocol wrapper -- convert errors thrown in execChatCommand into error responses to prevent aborting the protocol wrapper
RRChatResponse <$> execChatCommand (encodeUtf8 command) RRChatResponse <$> execChatCommand (encodeUtf8 command)
handleRecv :: Int -> TBQueue ChatResponse -> CM' RemoteResponse handleRecv :: Int -> TBQueue ChatResponse -> IO RemoteResponse
handleRecv time events = do handleRecv time events = do
logDebug' $ "Recv: " <> tshow time logDebug $ "Recv: " <> tshow time
RRChatEvent <$> (timeout time . atomically $ readTBQueue events) RRChatEvent <$> (timeout time . atomically $ readTBQueue events)
-- TODO this command could remember stored files and return IDs to allow removing files that are not needed. -- TODO this command could remember stored files and return IDs to allow removing files that are not needed.
+29 -20
View File
@@ -514,7 +514,7 @@ createRcvFileTransfer db userId Contact {contactId, localDisplayName = c} f@File
rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr
let rfdId = (\RcvFileDescr {fileDescrId} -> fileDescrId) <$> rfd_ let rfdId = (\RcvFileDescr {fileDescrId} -> fileDescrId) <$> rfd_
-- cryptoArgs = Nothing here, the decision to encrypt is made when receiving it -- cryptoArgs = Nothing here, the decision to encrypt is made when receiving it
xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False}) <$> rfd_ xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False, userApprovedRelays = False}) <$> rfd_
fileProtocol = if isJust rfd_ then FPXFTP else FPSMP fileProtocol = if isJust rfd_ then FPXFTP else FPSMP
fileId <- liftIO $ do fileId <- liftIO $ do
DB.execute DB.execute
@@ -535,7 +535,7 @@ createRcvGroupFileTransfer db userId GroupMember {groupId, groupMemberId, localD
rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr
let rfdId = (\RcvFileDescr {fileDescrId} -> fileDescrId) <$> rfd_ let rfdId = (\RcvFileDescr {fileDescrId} -> fileDescrId) <$> rfd_
-- cryptoArgs = Nothing here, the decision to encrypt is made when receiving it -- cryptoArgs = Nothing here, the decision to encrypt is made when receiving it
xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False}) <$> rfd_ xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False, userApprovedRelays = False}) <$> rfd_
fileProtocol = if isJust rfd_ then FPXFTP else FPSMP fileProtocol = if isJust rfd_ then FPXFTP else FPSMP
fileId <- liftIO $ do fileId <- liftIO $ do
DB.execute DB.execute
@@ -676,7 +676,9 @@ getRcvFileTransfer_ db userId fileId = do
[sql| [sql|
SELECT r.file_status, r.file_queue_info, r.group_member_id, f.file_name, SELECT r.file_status, r.file_queue_info, r.group_member_id, f.file_name,
f.file_size, f.chunk_size, f.cancelled, cs.local_display_name, m.local_display_name, f.file_size, f.chunk_size, f.cancelled, cs.local_display_name, m.local_display_name,
f.file_path, f.file_crypto_key, f.file_crypto_nonce, r.file_inline, r.rcv_file_inline, r.agent_rcv_file_id, r.agent_rcv_file_deleted, c.connection_id, c.agent_conn_id f.file_path, f.file_crypto_key, f.file_crypto_nonce, r.file_inline, r.rcv_file_inline,
r.agent_rcv_file_id, r.agent_rcv_file_deleted, r.user_approved_relays,
c.connection_id, c.agent_conn_id
FROM rcv_files r FROM rcv_files r
JOIN files f USING (file_id) JOIN files f USING (file_id)
LEFT JOIN connections c ON r.file_id = c.rcv_file_id LEFT JOIN connections c ON r.file_id = c.rcv_file_id
@@ -690,9 +692,9 @@ getRcvFileTransfer_ db userId fileId = do
where where
rcvFileTransfer :: rcvFileTransfer ::
Maybe RcvFileDescr -> Maybe RcvFileDescr ->
(FileStatus, Maybe ConnReqInvitation, Maybe Int64, String, Integer, Integer, Maybe Bool) :. (Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe InlineFileMode, Maybe InlineFileMode, Maybe AgentRcvFileId, Bool) :. (Maybe Int64, Maybe AgentConnId) -> (FileStatus, Maybe ConnReqInvitation, Maybe Int64, String, Integer, Integer, Maybe Bool) :. (Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe InlineFileMode, Maybe InlineFileMode, Maybe AgentRcvFileId, Bool, Bool) :. (Maybe Int64, Maybe AgentConnId) ->
ExceptT StoreError IO RcvFileTransfer ExceptT StoreError IO RcvFileTransfer
rcvFileTransfer rfd_ ((fileStatus', fileConnReq, grpMemberId, fileName, fileSize, chunkSize, cancelled_) :. (contactName_, memberName_, filePath_, fileKey, fileNonce, fileInline, rcvFileInline, agentRcvFileId, agentRcvFileDeleted) :. (connId_, agentConnId_)) = rcvFileTransfer rfd_ ((fileStatus', fileConnReq, grpMemberId, fileName, fileSize, chunkSize, cancelled_) :. (contactName_, memberName_, filePath_, fileKey, fileNonce, fileInline, rcvFileInline, agentRcvFileId, agentRcvFileDeleted, userApprovedRelays) :. (connId_, agentConnId_)) =
case contactName_ <|> memberName_ <|> standaloneName_ of case contactName_ <|> memberName_ <|> standaloneName_ of
Nothing -> throwError $ SERcvFileInvalid fileId Nothing -> throwError $ SERcvFileInvalid fileId
Just name -> Just name ->
@@ -709,7 +711,7 @@ getRcvFileTransfer_ db userId fileId = do
ft senderDisplayName fileStatus = ft senderDisplayName fileStatus =
let fileInvitation = FileInvitation {fileName, fileSize, fileDigest = Nothing, fileConnReq, fileInline, fileDescr = Nothing} let fileInvitation = FileInvitation {fileName, fileSize, fileDigest = Nothing, fileConnReq, fileInline, fileDescr = Nothing}
cryptoArgs = CFArgs <$> fileKey <*> fileNonce cryptoArgs = CFArgs <$> fileKey <*> fileNonce
xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId, agentRcvFileDeleted}) <$> rfd_ xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId, agentRcvFileDeleted, userApprovedRelays}) <$> rfd_
in RcvFileTransfer {fileId, xftpRcvFile, fileInvitation, fileStatus, rcvFileInline, senderDisplayName, chunkSize, cancelled, grpMemberId, cryptoArgs} in RcvFileTransfer {fileId, xftpRcvFile, fileInvitation, fileStatus, rcvFileInline, senderDisplayName, chunkSize, cancelled, grpMemberId, cryptoArgs}
rfi = maybe (throwError $ SERcvFileInvalid fileId) pure =<< rfi_ rfi = maybe (throwError $ SERcvFileInvalid fileId) pure =<< rfi_
rfi_ = case (filePath_, connId_, agentConnId_) of rfi_ = case (filePath_, connId_, agentConnId_) of
@@ -720,7 +722,7 @@ getRcvFileTransfer_ db userId fileId = do
acceptRcvFileTransfer :: DB.Connection -> VersionRangeChat -> User -> Int64 -> (CommandId, ConnId) -> ConnStatus -> FilePath -> SubscriptionMode -> ExceptT StoreError IO AChatItem acceptRcvFileTransfer :: DB.Connection -> VersionRangeChat -> User -> Int64 -> (CommandId, ConnId) -> ConnStatus -> FilePath -> SubscriptionMode -> ExceptT StoreError IO AChatItem
acceptRcvFileTransfer db vr user@User {userId} fileId (cmdId, acId) connStatus filePath subMode = ExceptT $ do acceptRcvFileTransfer db vr user@User {userId} fileId (cmdId, acId) connStatus filePath subMode = ExceptT $ do
currentTs <- getCurrentTime currentTs <- getCurrentTime
acceptRcvFT_ db user fileId filePath Nothing currentTs acceptRcvFT_ db user fileId filePath False Nothing currentTs
DB.execute DB.execute
db db
"INSERT INTO connections (agent_conn_id, conn_status, conn_type, rcv_file_id, user_id, created_at, updated_at, to_subscribe) VALUES (?,?,?,?,?,?,?,?)" "INSERT INTO connections (agent_conn_id, conn_status, conn_type, rcv_file_id, user_id, created_at, updated_at, to_subscribe) VALUES (?,?,?,?,?,?,?,?)"
@@ -740,33 +742,40 @@ getContactByFileId db vr user@User {userId} fileId = do
acceptRcvInlineFT :: DB.Connection -> VersionRangeChat -> User -> FileTransferId -> FilePath -> ExceptT StoreError IO AChatItem acceptRcvInlineFT :: DB.Connection -> VersionRangeChat -> User -> FileTransferId -> FilePath -> ExceptT StoreError IO AChatItem
acceptRcvInlineFT db vr user fileId filePath = do acceptRcvInlineFT db vr user fileId filePath = do
liftIO $ acceptRcvFT_ db user fileId filePath (Just IFMOffer) =<< getCurrentTime liftIO $ acceptRcvFT_ db user fileId filePath False (Just IFMOffer) =<< getCurrentTime
getChatItemByFileId db vr user fileId getChatItemByFileId db vr user fileId
startRcvInlineFT :: DB.Connection -> User -> RcvFileTransfer -> FilePath -> Maybe InlineFileMode -> IO () startRcvInlineFT :: DB.Connection -> User -> RcvFileTransfer -> FilePath -> Maybe InlineFileMode -> IO ()
startRcvInlineFT db user RcvFileTransfer {fileId} filePath rcvFileInline = startRcvInlineFT db user RcvFileTransfer {fileId} filePath rcvFileInline =
acceptRcvFT_ db user fileId filePath rcvFileInline =<< getCurrentTime acceptRcvFT_ db user fileId filePath False rcvFileInline =<< getCurrentTime
xftpAcceptRcvFT :: DB.Connection -> VersionRangeChat -> User -> FileTransferId -> FilePath -> ExceptT StoreError IO AChatItem xftpAcceptRcvFT :: DB.Connection -> VersionRangeChat -> User -> FileTransferId -> FilePath -> Bool -> ExceptT StoreError IO AChatItem
xftpAcceptRcvFT db vr user fileId filePath = do xftpAcceptRcvFT db vr user fileId filePath userApprovedRelays = do
liftIO $ acceptRcvFT_ db user fileId filePath Nothing =<< getCurrentTime liftIO $ acceptRcvFT_ db user fileId filePath userApprovedRelays Nothing =<< getCurrentTime
getChatItemByFileId db vr user fileId getChatItemByFileId db vr user fileId
acceptRcvFT_ :: DB.Connection -> User -> FileTransferId -> FilePath -> Maybe InlineFileMode -> UTCTime -> IO () acceptRcvFT_ :: DB.Connection -> User -> FileTransferId -> FilePath -> Bool -> Maybe InlineFileMode -> UTCTime -> IO ()
acceptRcvFT_ db User {userId} fileId filePath rcvFileInline currentTs = do acceptRcvFT_ db User {userId} fileId filePath userApprovedRelays rcvFileInline currentTs = do
DB.execute DB.execute
db db
"UPDATE files SET file_path = ?, ci_file_status = ?, updated_at = ? WHERE user_id = ? AND file_id = ?" "UPDATE files SET file_path = ?, ci_file_status = ?, updated_at = ? WHERE user_id = ? AND file_id = ?"
(filePath, CIFSRcvAccepted, currentTs, userId, fileId) (filePath, CIFSRcvAccepted, currentTs, userId, fileId)
DB.execute DB.execute
db db
"UPDATE rcv_files SET rcv_file_inline = ?, file_status = ?, updated_at = ? WHERE file_id = ?" "UPDATE rcv_files SET user_approved_relays = ?, rcv_file_inline = ?, file_status = ?, updated_at = ? WHERE file_id = ?"
(rcvFileInline, FSAccepted, currentTs, fileId) (userApprovedRelays, rcvFileInline, FSAccepted, currentTs, fileId)
setRcvFileToReceive :: DB.Connection -> FileTransferId -> Maybe CryptoFileArgs -> IO () setRcvFileToReceive :: DB.Connection -> FileTransferId -> Bool -> Maybe CryptoFileArgs -> IO ()
setRcvFileToReceive db fileId cfArgs_ = do setRcvFileToReceive db fileId userApprovedRelays cfArgs_ = do
currentTs <- getCurrentTime currentTs <- getCurrentTime
DB.execute db "UPDATE rcv_files SET to_receive = 1, updated_at = ? WHERE file_id = ?" (currentTs, fileId) DB.execute
db
[sql|
UPDATE rcv_files
SET to_receive = 1, user_approved_relays = ?, updated_at = ?
WHERE file_id = ?
|]
(userApprovedRelays, currentTs, fileId)
forM_ cfArgs_ $ \cfArgs -> setFileCryptoArgs_ db fileId cfArgs currentTs forM_ cfArgs_ $ \cfArgs -> setFileCryptoArgs_ db fileId cfArgs currentTs
setFileCryptoArgs :: DB.Connection -> FileTransferId -> CryptoFileArgs -> IO () setFileCryptoArgs :: DB.Connection -> FileTransferId -> CryptoFileArgs -> IO ()
@@ -950,7 +959,7 @@ getFileTransferMeta_ db userId fileId =
fileTransferMeta (fileName, fileSize, chunkSize, filePath, fileKey, fileNonce, fileInline, aSndFileId_, agentSndFileDeleted, privateSndFileDescr, cancelled_, xftpRedirectFor) = fileTransferMeta (fileName, fileSize, chunkSize, filePath, fileKey, fileNonce, fileInline, aSndFileId_, agentSndFileDeleted, privateSndFileDescr, cancelled_, xftpRedirectFor) =
let cryptoArgs = CFArgs <$> fileKey <*> fileNonce let cryptoArgs = CFArgs <$> fileKey <*> fileNonce
xftpSndFile = (\fId -> XFTPSndFile {agentSndFileId = fId, privateSndFileDescr, agentSndFileDeleted, cryptoArgs}) <$> aSndFileId_ xftpSndFile = (\fId -> XFTPSndFile {agentSndFileId = fId, privateSndFileDescr, agentSndFileDeleted, cryptoArgs}) <$> aSndFileId_
in FileTransferMeta {fileId, xftpSndFile, xftpRedirectFor, fileName, fileSize, chunkSize, filePath, fileInline, cancelled = fromMaybe False cancelled_} in FileTransferMeta {fileId, xftpSndFile, xftpRedirectFor, fileName, fileSize, chunkSize, filePath, fileInline, cancelled = fromMaybe False cancelled_}
lookupFileTransferRedirectMeta :: DB.Connection -> User -> Int64 -> IO [FileTransferMeta] lookupFileTransferRedirectMeta :: DB.Connection -> User -> Int64 -> IO [FileTransferMeta]
lookupFileTransferRedirectMeta db User {userId} fileId = do lookupFileTransferRedirectMeta db User {userId} fileId = do
+3 -1
View File
@@ -108,6 +108,7 @@ import Simplex.Chat.Migrations.M20240402_item_forwarded
import Simplex.Chat.Migrations.M20240430_ui_theme import Simplex.Chat.Migrations.M20240430_ui_theme
import Simplex.Chat.Migrations.M20240501_chat_deleted import Simplex.Chat.Migrations.M20240501_chat_deleted
import Simplex.Chat.Migrations.M20240510_chat_items_via_proxy import Simplex.Chat.Migrations.M20240510_chat_items_via_proxy
import Simplex.Chat.Migrations.M20240515_rcv_files_user_approved_relays
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)]
@@ -215,7 +216,8 @@ schemaMigrations =
("20240402_item_forwarded", m20240402_item_forwarded, Just down_m20240402_item_forwarded), ("20240402_item_forwarded", m20240402_item_forwarded, Just down_m20240402_item_forwarded),
("20240430_ui_theme", m20240430_ui_theme, Just down_m20240430_ui_theme), ("20240430_ui_theme", m20240430_ui_theme, Just down_m20240430_ui_theme),
("20240501_chat_deleted", m20240501_chat_deleted, Just down_m20240501_chat_deleted), ("20240501_chat_deleted", m20240501_chat_deleted, Just down_m20240501_chat_deleted),
("20240510_chat_items_via_proxy", m20240510_chat_items_via_proxy, Just down_m20240510_chat_items_via_proxy) ("20240510_chat_items_via_proxy", m20240510_chat_items_via_proxy, Just down_m20240510_chat_items_via_proxy),
("20240515_rcv_files_user_approved_relays", m20240515_rcv_files_user_approved_relays, Just down_m20240515_rcv_files_user_approved_relays)
] ]
-- | The list of migrations in ascending order by date -- | The list of migrations in ascending order by date
+1 -2
View File
@@ -48,8 +48,7 @@ simplexChatCLI cfg server_ = do
ts <- getCurrentTime ts <- getCurrentTime
tz <- getCurrentTimeZone tz <- getCurrentTimeZone
rh <- readTVarIO $ currentRemoteHost cc rh <- readTVarIO $ currentRemoteHost cc
ll <- readTVarIO $ appLogLevel cc putStrLn $ serializeChatResponse (rh, Just user) ts tz rh r
putStrLn $ serializeChatResponse (rh, Just user) ll ts tz rh r
welcome :: ChatOpts -> IO () welcome :: ChatOpts -> IO ()
welcome ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, networkConfig}} = welcome ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, networkConfig}} =
+5 -6
View File
@@ -10,6 +10,7 @@
module Simplex.Chat.Terminal.Output where module Simplex.Chat.Terminal.Output where
import Control.Concurrent (ThreadId) import Control.Concurrent (ThreadId)
import Control.Logger.Simple
import Control.Monad import Control.Monad
import Control.Monad.Catch (MonadMask) import Control.Monad.Catch (MonadMask)
import Control.Monad.Except import Control.Monad.Except
@@ -167,10 +168,9 @@ runTerminalOutput ct cc@ChatController {outputQ, showLiveItems, logFilePath} Cha
_ -> pure () _ -> pure ()
logResponse path s = withFile path AppendMode $ \h -> mapM_ (hPutStrLn h . unStyle) s logResponse path s = withFile path AppendMode $ \h -> mapM_ (hPutStrLn h . unStyle) s
getRemoteUser rhId = getRemoteUser rhId =
flip runReaderT cc $ runReaderT (execChatCommand (Just rhId) "/user") cc >>= \case
execChatCommand (Just rhId) "/user" >>= \case CRActiveUser {user} -> updateRemoteUser ct user rhId
CRActiveUser {user} -> liftIO $ updateRemoteUser ct user rhId cr -> logError $ "Unexpected reply while getting remote user: " <> tshow cr
cr -> logError' $ "Unexpected reply while getting remote user: " <> tshow cr
removeRemoteUser rhId = atomically $ TM.delete rhId (currentRemoteUsers ct) removeRemoteUser rhId = atomically $ TM.delete rhId (currentRemoteUsers ct)
responseNotification :: ChatTerminal -> ChatController -> ChatResponse -> IO () responseNotification :: ChatTerminal -> ChatController -> ChatResponse -> IO ()
@@ -275,8 +275,7 @@ responseString ct cc liveItems outputRH r = do
cu <- getCurrentUser ct cc cu <- getCurrentUser ct cc
ts <- getCurrentTime ts <- getCurrentTime
tz <- getCurrentTimeZone tz <- getCurrentTimeZone
ll <- readTVarIO $ appLogLevel cc pure $ responseToView cu (config cc) liveItems ts tz outputRH r
pure $ responseToView cu (config cc) ll liveItems ts tz outputRH r
updateRemoteUser :: ChatTerminal -> User -> RemoteHostId -> IO () updateRemoteUser :: ChatTerminal -> User -> RemoteHostId -> IO ()
updateRemoteUser ct user rhId = atomically $ TM.insert rhId user (currentRemoteUsers ct) updateRemoteUser ct user rhId = atomically $ TM.insert rhId user (currentRemoteUsers ct)
+2 -1
View File
@@ -1072,7 +1072,8 @@ data RcvFileTransfer = RcvFileTransfer
data XFTPRcvFile = XFTPRcvFile data XFTPRcvFile = XFTPRcvFile
{ rcvFileDescription :: RcvFileDescr, { rcvFileDescription :: RcvFileDescr,
agentRcvFileId :: Maybe AgentRcvFileId, agentRcvFileId :: Maybe AgentRcvFileId,
agentRcvFileDeleted :: Bool agentRcvFileDeleted :: Bool,
userApprovedRelays :: Bool
} }
deriving (Eq, Show) deriving (Eq, Show)
+9 -9
View File
@@ -79,11 +79,11 @@ data WCallCommand
$(JQ.deriveToJSON (taggedObjectJSON $ dropPrefix "WCCall") ''WCallCommand) $(JQ.deriveToJSON (taggedObjectJSON $ dropPrefix "WCCall") ''WCallCommand)
serializeChatResponse :: (Maybe RemoteHostId, Maybe User) -> ChatLogLevel -> CurrentTime -> TimeZone -> Maybe RemoteHostId -> ChatResponse -> String serializeChatResponse :: (Maybe RemoteHostId, Maybe User) -> CurrentTime -> TimeZone -> Maybe RemoteHostId -> ChatResponse -> String
serializeChatResponse user_ logLevel ts tz remoteHost_ = unlines . map unStyle . responseToView user_ defaultChatConfig logLevel False ts tz remoteHost_ serializeChatResponse user_ ts tz remoteHost_ = unlines . map unStyle . responseToView user_ defaultChatConfig False ts tz remoteHost_
responseToView :: (Maybe RemoteHostId, Maybe User) -> ChatConfig -> ChatLogLevel -> Bool -> CurrentTime -> TimeZone -> Maybe RemoteHostId -> ChatResponse -> [StyledString] responseToView :: (Maybe RemoteHostId, Maybe User) -> ChatConfig -> Bool -> CurrentTime -> TimeZone -> Maybe RemoteHostId -> ChatResponse -> [StyledString]
responseToView hu@(currentRH, user_) ChatConfig {showReactions, showReceipts, testView} logLevel liveItems ts tz outputRH = \case responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showReceipts, testView} liveItems ts tz outputRH = \case
CRActiveUser User {profile, uiThemes} -> viewUserProfile (fromLocalProfile profile) <> viewUITheme uiThemes CRActiveUser User {profile, uiThemes} -> viewUserProfile (fromLocalProfile profile) <> viewUITheme uiThemes
CRUsersList users -> viewUsersList users CRUsersList users -> viewUsersList users
CRChatStarted -> ["chat started"] CRChatStarted -> ["chat started"]
@@ -391,8 +391,6 @@ responseToView hu@(currentRH, user_) ChatConfig {showReactions, showReceipts, te
CRChatCmdError u e -> ttyUserPrefix' u $ viewChatError logLevel testView e CRChatCmdError u e -> ttyUserPrefix' u $ viewChatError logLevel testView e
CRChatError u e -> ttyUser' u $ viewChatError logLevel testView e CRChatError u e -> ttyUser' u $ viewChatError logLevel testView e
CRChatErrors u errs -> ttyUser' u $ concatMap (viewChatError logLevel testView) errs CRChatErrors u errs -> ttyUser' u $ concatMap (viewChatError logLevel testView) errs
CRAgentLog {} -> []
CRChatLog {} -> []
CRArchiveImported archiveErrs -> if null archiveErrs then ["ok"] else ["archive import errors: " <> plain (show archiveErrs)] CRArchiveImported archiveErrs -> if null archiveErrs then ["ok"] else ["archive import errors: " <> plain (show archiveErrs)]
CRAppSettings as -> ["app settings: " <> plain (LB.unpack $ J.encode as)] CRAppSettings as -> ["app settings: " <> plain (LB.unpack $ J.encode as)]
CRTimedAction _ _ -> [] CRTimedAction _ _ -> []
@@ -1768,6 +1766,7 @@ viewFileTransferStatusXFTP (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId
CIFSRcvInvitation -> ["receiving " <> fstr <> " not accepted yet, use " <> highlight ("/fr " <> show fileId) <> " to receive file"] CIFSRcvInvitation -> ["receiving " <> fstr <> " not accepted yet, use " <> highlight ("/fr " <> show fileId) <> " to receive file"]
CIFSRcvAccepted -> ["receiving " <> fstr <> " just started"] CIFSRcvAccepted -> ["receiving " <> fstr <> " just started"]
CIFSRcvTransfer progress total -> ["receiving " <> fstr <> " progress " <> fileProgressXFTP progress total fileSize] CIFSRcvTransfer progress total -> ["receiving " <> fstr <> " progress " <> fileProgressXFTP progress total fileSize]
CIFSRcvAborted -> ["receiving " <> fstr <> " aborted, use " <> highlight ("/fr " <> show fileId) <> " to receive file"]
CIFSRcvComplete -> ["receiving " <> fstr <> " complete" <> maybe "" (\(CryptoFile fp _) -> ", path: " <> plain fp) fileSource] CIFSRcvComplete -> ["receiving " <> fstr <> " complete" <> maybe "" (\(CryptoFile fp _) -> ", path: " <> plain fp) fileSource]
CIFSRcvCancelled -> ["receiving " <> fstr <> " cancelled"] CIFSRcvCancelled -> ["receiving " <> fstr <> " cancelled"]
CIFSRcvError -> ["receiving " <> fstr <> " error"] CIFSRcvError -> ["receiving " <> fstr <> " error"]
@@ -1971,8 +1970,9 @@ viewChatError logLevel testView = \case
CEFileImageType _ -> ["image type must be jpg, send as a file using " <> highlight' "/f"] CEFileImageType _ -> ["image type must be jpg, send as a file using " <> highlight' "/f"]
CEFileImageSize _ -> ["max image size: " <> sShow maxImageSize <> " bytes, resize it or send as a file using " <> highlight' "/f"] CEFileImageSize _ -> ["max image size: " <> sShow maxImageSize <> " bytes, resize it or send as a file using " <> highlight' "/f"]
CEFileNotReceived fileId -> ["file " <> sShow fileId <> " not received"] CEFileNotReceived fileId -> ["file " <> sShow fileId <> " not received"]
CEXFTPRcvFile fileId aFileId e -> ["error receiving XFTP file " <> sShow fileId <> ", agent file id " <> sShow aFileId <> ": " <> sShow e | logLevel <= CLLError] CEFileNotApproved fileId unknownSrvs -> ["file " <> sShow fileId <> " aborted, unknwon XFTP servers:"] <> map (plain . show) unknownSrvs
CEXFTPSndFile fileId aFileId e -> ["error sending XFTP file " <> sShow fileId <> ", agent file id " <> sShow aFileId <> ": " <> sShow e | logLevel <= CLLError] CEXFTPRcvFile fileId aFileId e -> ["error receiving XFTP file " <> sShow fileId <> ", agent file id " <> sShow aFileId <> ": " <> sShow e | logLevel == CLLError]
CEXFTPSndFile fileId aFileId e -> ["error sending XFTP file " <> sShow fileId <> ", agent file id " <> sShow aFileId <> ": " <> sShow e | logLevel == CLLError]
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"]
@@ -2034,7 +2034,7 @@ viewChatError logLevel testView = \case
<> "error: connection authorization failed - this could happen if connection was deleted,\ <> "error: connection authorization failed - this could happen if connection was deleted,\
\ secured with different credentials, or due to a bug - please re-create the connection" \ secured with different credentials, or due to a bug - please re-create the connection"
] ]
AGENT A_DUPLICATE -> [withConnEntity <> "error: AGENT A_DUPLICATE" | logLevel <= CLLDebug] AGENT A_DUPLICATE -> [withConnEntity <> "error: AGENT A_DUPLICATE" | logLevel == CLLDebug]
AGENT A_PROHIBITED -> [withConnEntity <> "error: AGENT A_PROHIBITED" | logLevel <= CLLWarning] AGENT A_PROHIBITED -> [withConnEntity <> "error: AGENT A_PROHIBITED" | logLevel <= CLLWarning]
CONN NOT_FOUND -> [withConnEntity <> "error: CONN NOT_FOUND" | logLevel <= CLLWarning] CONN NOT_FOUND -> [withConnEntity <> "error: CONN NOT_FOUND" | logLevel <= CLLWarning]
CRITICAL restart e -> [plain $ "critical error: " <> e] <> ["please restart the app" | restart] CRITICAL restart e -> [plain $ "critical error: " <> e] <> ["please restart the app" | restart]