core, ui: errors for blocked files and contact addresses (#5510)

* core, ui: errors for blocked files and contact addresses

* android

* iOS: How it works, stub for blog post

* android: blocked errors WIP

* android: alert with button

* update

* fix encoding

* nix

* simplexmq
This commit is contained in:
Evgeny 2025-01-12 21:25:25 +00:00 committed by GitHub
parent 0d6b26c269
commit eacae74fed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 216 additions and 140 deletions

View file

@ -852,6 +852,18 @@ func apiConnect_(incognito: Bool, connReq: String) async -> ((ConnReqType, Pendi
message: "Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection."
)
return (nil, alert)
case let .chatCmdError(_, .errorAgent(.SMP(_, .BLOCKED(info)))):
let alert = Alert(
title: Text("Connection blocked"),
message: Text("Connection is blocked by server operator:\n\(info.reason.text)"),
primaryButton: .default(Text("Ok")),
secondaryButton: .default(Text("How it works")) {
DispatchQueue.main.async {
UIApplication.shared.open(contentModerationPostLink)
}
}
)
return (nil, alert)
case .chatCmdError(_, .errorAgent(.SMP(_, .QUOTA))):
let alert = mkAlert(
title: "Undelivered messages",

View file

@ -118,16 +118,10 @@ struct CIFileView: View {
}
case let .rcvError(rcvFileError):
logger.debug("CIFileView fileAction - in .rcvError")
AlertManager.shared.showAlert(Alert(
title: Text("File error"),
message: Text(rcvFileError.errorInfo)
))
showFileErrorAlert(rcvFileError)
case let .rcvWarning(rcvFileError):
logger.debug("CIFileView fileAction - in .rcvWarning")
AlertManager.shared.showAlert(Alert(
title: Text("Temporary file error"),
message: Text(rcvFileError.errorInfo)
))
showFileErrorAlert(rcvFileError, temporary: true)
case .sndStored:
logger.debug("CIFileView fileAction - in .sndStored")
if file.fileProtocol == .local, let fileSource = getLoadedFileSource(file) {
@ -140,16 +134,10 @@ struct CIFileView: View {
}
case let .sndError(sndFileError):
logger.debug("CIFileView fileAction - in .sndError")
AlertManager.shared.showAlert(Alert(
title: Text("File error"),
message: Text(sndFileError.errorInfo)
))
showFileErrorAlert(sndFileError)
case let .sndWarning(sndFileError):
logger.debug("CIFileView fileAction - in .sndWarning")
AlertManager.shared.showAlert(Alert(
title: Text("Temporary file error"),
message: Text(sndFileError.errorInfo)
))
showFileErrorAlert(sndFileError, temporary: true)
default: break
}
}
@ -268,6 +256,26 @@ func saveCryptoFile(_ fileSource: CryptoFile) {
}
}
func showFileErrorAlert(_ err: FileError, temporary: Bool = false) {
let title: String = if temporary {
NSLocalizedString("Temporary file error", comment: "file error alert title")
} else {
NSLocalizedString("File error", comment: "file error alert title")
}
if let btn = err.moreInfoButton {
showAlert(title, message: err.errorInfo) {
[
okAlertAction,
UIAlertAction(title: NSLocalizedString("How it works", comment: "alert button"), style: .default, handler: { _ in
UIApplication.shared.open(contentModerationPostLink)
})
]
}
} else {
showAlert(title, message: err.errorInfo)
}
}
struct CIFileView_Previews: PreviewProvider {
static var previews: some View {
let sentFile: ChatItem = ChatItem(

View file

@ -69,25 +69,13 @@ struct CIImageView: View {
case .rcvComplete: () // ?
case .rcvCancelled: () // TODO
case let .rcvError(rcvFileError):
AlertManager.shared.showAlert(Alert(
title: Text("File error"),
message: Text(rcvFileError.errorInfo)
))
showFileErrorAlert(rcvFileError)
case let .rcvWarning(rcvFileError):
AlertManager.shared.showAlert(Alert(
title: Text("Temporary file error"),
message: Text(rcvFileError.errorInfo)
))
showFileErrorAlert(rcvFileError, temporary: true)
case let .sndError(sndFileError):
AlertManager.shared.showAlert(Alert(
title: Text("File error"),
message: Text(sndFileError.errorInfo)
))
showFileErrorAlert(sndFileError)
case let .sndWarning(sndFileError):
AlertManager.shared.showAlert(Alert(
title: Text("Temporary file error"),
message: Text(sndFileError.errorInfo)
))
showFileErrorAlert(sndFileError, temporary: true)
default: ()
}
}

View file

@ -355,18 +355,12 @@ struct CIVideoView: View {
case let .sndError(sndFileError):
fileIcon("xmark", 10, 13)
.onTapGesture {
AlertManager.shared.showAlert(Alert(
title: Text("File error"),
message: Text(sndFileError.errorInfo)
))
showFileErrorAlert(sndFileError)
}
case let .sndWarning(sndFileError):
fileIcon("exclamationmark.triangle.fill", 10, 13)
.onTapGesture {
AlertManager.shared.showAlert(Alert(
title: Text("Temporary file error"),
message: Text(sndFileError.errorInfo)
))
showFileErrorAlert(sndFileError, temporary: true)
}
case .rcvInvitation: fileIcon("arrow.down", 10, 13)
case .rcvAccepted: fileIcon("ellipsis", 14, 11)
@ -382,18 +376,12 @@ struct CIVideoView: View {
case let .rcvError(rcvFileError):
fileIcon("xmark", 10, 13)
.onTapGesture {
AlertManager.shared.showAlert(Alert(
title: Text("File error"),
message: Text(rcvFileError.errorInfo)
))
showFileErrorAlert(rcvFileError)
}
case let .rcvWarning(rcvFileError):
fileIcon("exclamationmark.triangle.fill", 10, 13)
.onTapGesture {
AlertManager.shared.showAlert(Alert(
title: Text("Temporary file error"),
message: Text(rcvFileError.errorInfo)
))
showFileErrorAlert(rcvFileError, temporary: true)
}
case .invalid: fileIcon("questionmark", 10, 13)
}

View file

@ -169,18 +169,12 @@ struct VoiceMessagePlayer: View {
case let .sndError(sndFileError):
fileStatusIcon("multiply", 14)
.onTapGesture {
AlertManager.shared.showAlert(Alert(
title: Text("File error"),
message: Text(sndFileError.errorInfo)
))
showFileErrorAlert(sndFileError)
}
case let .sndWarning(sndFileError):
fileStatusIcon("exclamationmark.triangle.fill", 16)
.onTapGesture {
AlertManager.shared.showAlert(Alert(
title: Text("Temporary file error"),
message: Text(sndFileError.errorInfo)
))
showFileErrorAlert(sndFileError, temporary: true)
}
case .rcvInvitation: downloadButton(recordingFile, "play.fill")
case .rcvAccepted: loadingIcon()
@ -191,18 +185,12 @@ struct VoiceMessagePlayer: View {
case let .rcvError(rcvFileError):
fileStatusIcon("multiply", 14)
.onTapGesture {
AlertManager.shared.showAlert(Alert(
title: Text("File error"),
message: Text(rcvFileError.errorInfo)
))
showFileErrorAlert(rcvFileError)
}
case let .rcvWarning(rcvFileError):
fileStatusIcon("exclamationmark.triangle.fill", 16)
.onTapGesture {
AlertManager.shared.showAlert(Alert(
title: Text("Temporary file error"),
message: Text(rcvFileError.errorInfo)
))
showFileErrorAlert(rcvFileError, temporary: true)
}
case .invalid: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel))
}

View file

@ -2481,6 +2481,7 @@ public enum ProtocolErrorType: Decodable, Hashable {
case CMD(cmdErr: ProtocolCommandError)
indirect case PROXY(proxyErr: ProxyError)
case AUTH
case BLOCKED(blockInfo: BlockingInfo)
case CRYPTO
case QUOTA
case STORE(storeErr: String)
@ -2497,11 +2498,28 @@ public enum ProxyError: Decodable, Hashable {
case NO_SESSION
}
public struct BlockingInfo: Decodable, Equatable, Hashable {
public var reason: BlockingReason
}
public enum BlockingReason: String, Decodable {
case spam
case content
public var text: String {
switch self {
case .spam: NSLocalizedString("Spam", comment: "blocking reason")
case .content: NSLocalizedString("Content violates conditions of use", comment: "blocking reason")
}
}
}
public enum XFTPErrorType: Decodable, Hashable {
case BLOCK
case SESSION
case CMD(cmdErr: ProtocolCommandError)
case AUTH
case BLOCKED(blockInfo: BlockingInfo)
case SIZE
case QUOTA
case DIGEST

View file

@ -15,6 +15,8 @@ public let CREATE_MEMBER_CONTACT_VERSION = 2
// version to receive reports (MCReport)
public let REPORTS_VERSION = 12
public let contentModerationPostLink = URL(string: "https://simplex.chat/blog/20250112-simplex-network-privacy-preserving-content-moderation.html")!
public struct User: Identifiable, Decodable, UserLike, NamedChat, Hashable {
public var userId: Int64
public var agentUserId: String
@ -3024,7 +3026,7 @@ public enum SndError: Decodable, Hashable {
case proxyRelay(proxyServer: String, srvError: SrvError)
case other(sndError: String)
public var errorInfo: String {
public var errorInfo: String {
switch self {
case .auth: NSLocalizedString("Wrong key or unknown connection - most likely this connection is deleted.", comment: "snd error text")
case .quota: NSLocalizedString("Capacity exceeded - recipient did not receive previously sent messages.", comment: "snd error text")
@ -3684,6 +3686,7 @@ public enum CIFileStatus: Decodable, Equatable, Hashable {
public enum FileError: Decodable, Equatable, Hashable {
case auth
case blocked(server: String, blockInfo: BlockingInfo)
case noFile
case relay(srvError: SrvError)
case other(fileError: String)
@ -3691,6 +3694,7 @@ public enum FileError: Decodable, Equatable, Hashable {
var id: String {
switch self {
case .auth: return "auth"
case let .blocked(srv, info): return "blocked \(srv) \(info)"
case .noFile: return "noFile"
case let .relay(srvError): return "relay \(srvError)"
case let .other(fileError): return "other \(fileError)"
@ -3700,11 +3704,19 @@ public enum FileError: Decodable, Equatable, Hashable {
public var errorInfo: String {
switch self {
case .auth: NSLocalizedString("Wrong key or unknown file chunk address - most likely file is deleted.", comment: "file error text")
case let .blocked(_, info): NSLocalizedString("File is blocked by server operator:\n\(info.reason.text).", comment: "file error text")
case .noFile: NSLocalizedString("File not found - most likely file was deleted or cancelled.", comment: "file error text")
case let .relay(srvError): String.localizedStringWithFormat(NSLocalizedString("File server error: %@", comment: "file error text"), srvError.errorInfo)
case let .other(fileError): String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: "file error text"), fileError)
}
}
public var moreInfoButton: (label: LocalizedStringKey, link: URL)? {
switch self {
case .blocked: ("How it works", contentModerationPostLink)
default: nil
}
}
}
public enum MsgContent: Equatable, Hashable {

View file

@ -12,6 +12,7 @@ import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.*
import chat.simplex.common.views.chat.*
import chat.simplex.common.views.chat.item.contentModerationPostLink
import chat.simplex.common.views.chatlist.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.migration.MigrationToDeviceState
@ -22,7 +23,6 @@ import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.collections.removeAll as remAll
import kotlinx.datetime.*
import kotlinx.datetime.TimeZone
@ -3591,15 +3591,22 @@ sealed class CIFileStatus {
@Serializable
sealed class FileError {
@Serializable @SerialName("auth") class Auth: FileError()
@Serializable @SerialName("blocked") class Blocked(val server: String, val blockInfo: BlockingInfo): FileError()
@Serializable @SerialName("noFile") class NoFile: FileError()
@Serializable @SerialName("relay") class Relay(val srvError: SrvError): FileError()
@Serializable @SerialName("other") class Other(val fileError: String): FileError()
val errorInfo: String get() = when (this) {
is FileError.Auth -> generalGetString(MR.strings.file_error_auth)
is FileError.NoFile -> generalGetString(MR.strings.file_error_no_file)
is FileError.Relay -> generalGetString(MR.strings.file_error_relay).format(srvError.errorInfo)
is FileError.Other -> generalGetString(MR.strings.ci_status_other_error).format(fileError)
is Auth -> generalGetString(MR.strings.file_error_auth)
is Blocked -> generalGetString(MR.strings.file_error_blocked).format(blockInfo.reason.text)
is NoFile -> generalGetString(MR.strings.file_error_no_file)
is Relay -> generalGetString(MR.strings.file_error_relay).format(srvError.errorInfo)
is Other -> generalGetString(MR.strings.ci_status_other_error).format(fileError)
}
val moreInfoButton: Pair<String, String>? get() = when(this) {
is Blocked -> generalGetString(MR.strings.how_it_works) to contentModerationPostLink
else -> null
}
}

View file

@ -19,10 +19,12 @@ import chat.simplex.common.model.ChatController.setNetCfg
import chat.simplex.common.model.ChatModel.changingActiveUserMutex
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen
import chat.simplex.common.model.SMPErrorType.BLOCKED
import dev.icerock.moko.resources.compose.painterResource
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.*
import chat.simplex.common.views.chat.item.showContentBlockedAlert
import chat.simplex.common.views.chat.item.showQuotedItemDoesNotExistAlert
import chat.simplex.common.views.chatlist.openGroupChat
import chat.simplex.common.views.migration.MigrationFileLinkData
@ -1411,6 +1413,15 @@ object ChatController {
)
return null
}
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent
&& r.chatError.agentError is AgentErrorType.SMP
&& r.chatError.agentError.smpErr is SMPErrorType.BLOCKED -> {
showContentBlockedAlert(
generalGetString(MR.strings.connection_error_blocked),
generalGetString(MR.strings.connection_error_blocked_desc).format(r.chatError.agentError.smpErr.blockInfo.reason.text),
)
return null
}
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent
&& r.chatError.agentError is AgentErrorType.SMP
&& r.chatError.agentError.smpErr is SMPErrorType.QUOTA -> {
@ -6756,6 +6767,7 @@ sealed class BrokerErrorType {
@Serializable @SerialName("TIMEOUT") object TIMEOUT: BrokerErrorType()
}
// ProtocolErrorType
@Serializable
sealed class SMPErrorType {
val string: String get() = when (this) {
@ -6764,9 +6776,10 @@ sealed class SMPErrorType {
is CMD -> "CMD ${cmdErr.string}"
is PROXY -> "PROXY ${proxyErr.string}"
is AUTH -> "AUTH"
is BLOCKED -> "BLOCKED ${json.encodeToString(blockInfo)}"
is CRYPTO -> "CRYPTO"
is QUOTA -> "QUOTA"
is STORE -> "STORE ${storeErr}"
is STORE -> "STORE $storeErr"
is NO_MSG -> "NO_MSG"
is LARGE_MSG -> "LARGE_MSG"
is EXPIRED -> "EXPIRED"
@ -6777,6 +6790,7 @@ sealed class SMPErrorType {
@Serializable @SerialName("CMD") class CMD(val cmdErr: ProtocolCommandError): SMPErrorType()
@Serializable @SerialName("PROXY") class PROXY(val proxyErr: ProxyError): SMPErrorType()
@Serializable @SerialName("AUTH") class AUTH: SMPErrorType()
@Serializable @SerialName("BLOCKED") class BLOCKED(val blockInfo: BlockingInfo): SMPErrorType()
@Serializable @SerialName("CRYPTO") class CRYPTO: SMPErrorType()
@Serializable @SerialName("QUOTA") class QUOTA: SMPErrorType()
@Serializable @SerialName("STORE") class STORE(val storeErr: String): SMPErrorType()
@ -6800,6 +6814,22 @@ sealed class ProxyError {
@Serializable @SerialName("NO_SESSION") class NO_SESSION: ProxyError()
}
@Serializable
data class BlockingInfo(
val reason: BlockingReason
)
@Serializable
enum class BlockingReason {
@SerialName("spam") Spam,
@SerialName("content") Content;
val text: String get() = when (this) {
Spam -> generalGetString(MR.strings.blocking_reason_spam)
Content -> generalGetString(MR.strings.blocking_reason_content)
}
}
@Serializable
sealed class ProtocolCommandError {
val string: String get() = when (this) {
@ -6875,6 +6905,7 @@ sealed class XFTPErrorType {
is SESSION -> "SESSION"
is CMD -> "CMD ${cmdErr.string}"
is AUTH -> "AUTH"
is BLOCKED -> "BLOCKED ${json.encodeToString(blockInfo)}"
is SIZE -> "SIZE"
is QUOTA -> "QUOTA"
is DIGEST -> "DIGEST"
@ -6890,6 +6921,7 @@ sealed class XFTPErrorType {
@Serializable @SerialName("SESSION") object SESSION: XFTPErrorType()
@Serializable @SerialName("CMD") class CMD(val cmdErr: ProtocolCommandError): XFTPErrorType()
@Serializable @SerialName("AUTH") object AUTH: XFTPErrorType()
@Serializable @SerialName("BLOCKED") class BLOCKED(val blockInfo: BlockingInfo): XFTPErrorType()
@Serializable @SerialName("SIZE") object SIZE: XFTPErrorType()
@Serializable @SerialName("QUOTA") object QUOTA: XFTPErrorType()
@Serializable @SerialName("DIGEST") object DIGEST: XFTPErrorType()

View file

@ -1,5 +1,6 @@
package chat.simplex.common.views.chat.item
import SectionItemView
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize
@ -13,6 +14,8 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.*
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@ -92,25 +95,13 @@ fun CIFileView(
FileProtocol.LOCAL -> {}
}
file.fileStatus is CIFileStatus.RcvError ->
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.file_error),
file.fileStatus.rcvFileError.errorInfo
)
showFileErrorAlert(file.fileStatus.rcvFileError)
file.fileStatus is CIFileStatus.RcvWarning ->
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.temporary_file_error),
file.fileStatus.rcvFileError.errorInfo
)
showFileErrorAlert(file.fileStatus.rcvFileError, temporary = true)
file.fileStatus is CIFileStatus.SndError ->
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.file_error),
file.fileStatus.sndFileError.errorInfo
)
showFileErrorAlert(file.fileStatus.sndFileError)
file.fileStatus is CIFileStatus.SndWarning ->
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.temporary_file_error),
file.fileStatus.sndFileError.errorInfo
)
showFileErrorAlert(file.fileStatus.sndFileError, temporary = true)
file.forwardingAllowed() -> {
withLongRunningApi(slow = 600_000) {
var filePath = getLoadedFilePath(file)
@ -235,6 +226,37 @@ fun CIFileView(
fun fileSizeValid(file: CIFile): Boolean = file.fileSize <= getMaxFileSize(file.fileProtocol)
fun showFileErrorAlert(err: FileError, temporary: Boolean = false) {
val title: String = generalGetString(if (temporary) MR.strings.temporary_file_error else MR.strings.file_error)
val btn = err.moreInfoButton
if (btn != null) {
showContentBlockedAlert(title, err.errorInfo)
} else {
AlertManager.shared.showAlertMsg(title, err.errorInfo)
}
}
val contentModerationPostLink = "https://simplex.chat/blog/20250112-simplex-network-privacy-preserving-content-moderation.html"
fun showContentBlockedAlert(title: String, message: String) {
AlertManager.shared.showAlertDialogButtonsColumn(title, text = message, buttons = {
val uriHandler = LocalUriHandler.current
Column {
SectionItemView({
AlertManager.shared.hideAlert()
uriHandler.openUriCatching(contentModerationPostLink)
}) {
Text(generalGetString(MR.strings.how_it_works), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
SectionItemView({
AlertManager.shared.hideAlert()
}) {
Text(generalGetString(MR.strings.ok), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
}
})
}
@Composable
expect fun SaveOrOpenFileMenu(
showMenu: MutableState<Boolean>,

View file

@ -238,25 +238,13 @@ fun CIImageView(
FileProtocol.LOCAL -> {}
}
file.fileStatus is CIFileStatus.RcvError ->
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.file_error),
file.fileStatus.rcvFileError.errorInfo
)
showFileErrorAlert(file.fileStatus.rcvFileError)
file.fileStatus is CIFileStatus.RcvWarning ->
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.temporary_file_error),
file.fileStatus.rcvFileError.errorInfo
)
showFileErrorAlert(file.fileStatus.rcvFileError, temporary = true)
file.fileStatus is CIFileStatus.SndError ->
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.file_error),
file.fileStatus.sndFileError.errorInfo
)
showFileErrorAlert(file.fileStatus.sndFileError)
file.fileStatus is CIFileStatus.SndWarning ->
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.temporary_file_error),
file.fileStatus.sndFileError.errorInfo
)
showFileErrorAlert(file.fileStatus.sndFileError, temporary = true)
file.fileStatus is CIFileStatus.RcvTransfer -> {} // ?
file.fileStatus is CIFileStatus.RcvComplete -> {} // ?
file.fileStatus is CIFileStatus.RcvCancelled -> {} // TODO

View file

@ -499,10 +499,7 @@ private fun fileStatusIcon(file: CIFile?, smallView: Boolean) {
painterResource(MR.images.ic_close),
MR.strings.icon_descr_file,
onClick = {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.file_error),
file.fileStatus.sndFileError.errorInfo
)
showFileErrorAlert(file.fileStatus.sndFileError)
}
)
is CIFileStatus.SndWarning ->
@ -510,10 +507,7 @@ private fun fileStatusIcon(file: CIFile?, smallView: Boolean) {
painterResource(MR.images.ic_warning_filled),
MR.strings.icon_descr_file,
onClick = {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.temporary_file_error),
file.fileStatus.sndFileError.errorInfo
)
showFileErrorAlert(file.fileStatus.sndFileError, temporary = true)
}
)
is CIFileStatus.RcvInvitation -> fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_video_asked_to_receive)
@ -532,10 +526,7 @@ private fun fileStatusIcon(file: CIFile?, smallView: Boolean) {
painterResource(MR.images.ic_close),
MR.strings.icon_descr_file,
onClick = {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.file_error),
file.fileStatus.rcvFileError.errorInfo
)
showFileErrorAlert(file.fileStatus.rcvFileError)
}
)
is CIFileStatus.RcvWarning ->
@ -543,10 +534,7 @@ private fun fileStatusIcon(file: CIFile?, smallView: Boolean) {
painterResource(MR.images.ic_warning_filled),
MR.strings.icon_descr_file,
onClick = {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.temporary_file_error),
file.fileStatus.rcvFileError.errorInfo
)
showFileErrorAlert(file.fileStatus.rcvFileError, temporary = true)
}
)
is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file)

View file

@ -398,10 +398,7 @@ private fun VoiceMsgIndicator(
sizeMultiplier,
longClick,
onClick = {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.file_error),
file.fileStatus.sndFileError.errorInfo
)
showFileErrorAlert(file.fileStatus.sndFileError)
}
)
file != null && file.fileStatus is CIFileStatus.SndWarning ->
@ -411,10 +408,7 @@ private fun VoiceMsgIndicator(
sizeMultiplier,
longClick,
onClick = {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.temporary_file_error),
file.fileStatus.sndFileError.errorInfo
)
showFileErrorAlert(file.fileStatus.sndFileError, temporary = true)
}
)
file?.fileStatus is CIFileStatus.RcvInvitation ->
@ -430,10 +424,7 @@ private fun VoiceMsgIndicator(
sizeMultiplier,
longClick,
onClick = {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.file_error),
file.fileStatus.rcvFileError.errorInfo
)
showFileErrorAlert(file.fileStatus.rcvFileError)
}
)
file != null && file.fileStatus is CIFileStatus.RcvWarning ->
@ -443,10 +434,7 @@ private fun VoiceMsgIndicator(
sizeMultiplier,
longClick,
onClick = {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.temporary_file_error),
file.fileStatus.rcvFileError.errorInfo
)
showFileErrorAlert(file.fileStatus.rcvFileError, temporary = true)
}
)
file != null && file.loaded && progress != null && duration != null ->

View file

@ -132,6 +132,8 @@
<string name="for_chat_profile">For chat profile %s:</string>
<string name="errors_in_servers_configuration">Errors in servers configuration.</string>
<string name="error_accepting_operator_conditions">Error accepting conditions</string>
<string name="blocking_reason_spam">Spam</string>
<string name="blocking_reason_content">Content violates conditions of use</string>
<!-- API Error Responses - SimpleXAPI.kt -->
<string name="connection_timeout">Connection timeout</string>
@ -168,6 +170,8 @@
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Please check that you used the correct link or ask your contact to send you another one.</string>
<string name="connection_error_auth">Connection error (AUTH)</string>
<string name="connection_error_auth_desc">Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection.</string>
<string name="connection_error_blocked">Connection blocked</string>
<string name="connection_error_blocked_desc">Connection is blocked by server operator:\n%1$s.</string>
<string name="connection_error_quota">Undelivered messages</string>
<string name="connection_error_quota_desc">The connection reached the limit of undelivered messages, your contact may be offline.</string>
<string name="error_accepting_contact_request">Error accepting contact request</string>
@ -323,6 +327,7 @@
<!-- CIFileStatus errors -->
<string name="file_error_auth">Wrong key or unknown file chunk address - most likely file is deleted.</string>
<string name="file_error_blocked">File is blocked by server operator:\n%1$s.</string>
<string name="file_error_no_file">File not found - most likely file was deleted or cancelled.</string>
<string name="file_error_relay">File server error: %1$s</string>

View file

@ -0,0 +1,22 @@
---
layout: layouts/article.html
title: "SimpleX network: privacy preserving content moderation"
date: 2024-12-18
preview: How network operators prevent distribution of CSAM without compromising users privacy and security.
# image: images/20241218-pub.jpg
# imageWide: true
draft: true
permalink: "/blog/20250112-simplex-network-privacy-preserving-content-moderation.html"
---
# SimpleX network: privacy preserving content moderation
**Will be published:** Jan 12, 2025
This blog post will cover our approach to removing CSAM that has:
- NO user identification, thus preserving privacy of the users.
- NO client- or server-side content scanning, thus preserving privacy and security of e2e encryption.
The current and future content restriction will only be applied based on the users' complaints, and only to the content that can be accessed by server operators via public channels.
Please read this document: https://github.com/simplex-chat/simplex-chat/blob/stable/docs/rfcs/2024-12-30-content-moderation.md

View file

@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
tag: 9d9ec8cd0b171b2058c59c4e7292ccafa96b6e2b
tag: 3d4e0b06c04a13555c55c2e0efde56f9f78e7ea1
source-repository-package
type: git

View file

@ -1,5 +1,5 @@
{
"https://github.com/simplex-chat/simplexmq.git"."9d9ec8cd0b171b2058c59c4e7292ccafa96b6e2b" = "0mvg9yrwb835vf2kz8k0ac4i7vzjpvbpcwg895n3kcfdkdcnxh14";
"https://github.com/simplex-chat/simplexmq.git"."3d4e0b06c04a13555c55c2e0efde56f9f78e7ea1" = "0l194fm6kxy54gkyz0lhvba3cxgjdg812qwpjki5kwfmhhliys6q";
"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/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";

View file

@ -284,6 +284,7 @@ processAgentMsgSndFile _corrId aFileId msg = do
agentFileError :: AgentErrorType -> FileError
agentFileError = \case
XFTP _ XFTP.AUTH -> FileErrAuth
XFTP srv (XFTP.BLOCKED info) -> FileErrBlocked srv info
FILE NO_FILE -> FileErrNoFile
BROKER _ e -> brokerError FileErrRelay e
e -> FileErrOther $ tshow e

View file

@ -52,7 +52,7 @@ import Simplex.Messaging.Crypto.File (CryptoFile (..))
import qualified Simplex.Messaging.Crypto.File as CF
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_, parseAll, sumTypeJSON)
import Simplex.Messaging.Protocol (MsgBody)
import Simplex.Messaging.Protocol (BlockingInfo, MsgBody)
import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, (<$?>))
#if defined(dbPostgres)
import Database.PostgreSQL.Simple.FromField (FromField (..))
@ -741,6 +741,7 @@ aciFileStatusJSON = \case
data FileError
= FileErrAuth
| FileErrBlocked {server :: String, blockInfo :: BlockingInfo}
| FileErrNoFile
| FileErrRelay {srvError :: SrvError}
| FileErrOther {fileError :: Text}
@ -749,14 +750,16 @@ data FileError
instance StrEncoding FileError where
strEncode = \case
FileErrAuth -> "auth"
FileErrBlocked srv info -> "blocked " <> strEncode (srv, info)
FileErrNoFile -> "no_file"
FileErrRelay srvErr -> "relay " <> strEncode srvErr
FileErrOther e -> "other " <> encodeUtf8 e
strP =
A.takeWhile1 (/= ' ') >>= \case
"auth" -> pure FileErrAuth
"blocked" -> FileErrBlocked <$> _strP <*> _strP
"no_file" -> pure FileErrNoFile
"relay" -> FileErrRelay <$> (A.space *> strP)
"relay" -> FileErrRelay <$> _strP
"other" -> FileErrOther . safeDecodeUtf8 <$> (A.space *> A.takeByteString)
s -> FileErrOther . safeDecodeUtf8 . (s <>) <$> A.takeByteString

View file

@ -66,7 +66,7 @@ import qualified Simplex.Messaging.Crypto.Ratchet as CR
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (dropPrefix, taggedObjectJSON)
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType, ProtocolServer (..), ProtocolTypeI, SProtocolType (..), UserProtocol)
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType, BlockingInfo (..), BlockingReason (..), ProtocolServer (..), ProtocolTypeI, SProtocolType (..), UserProtocol)
import qualified Simplex.Messaging.Protocol as SMP
import Simplex.Messaging.Transport.Client (TransportHost (..))
import Simplex.Messaging.Util (safeDecodeUtf8, tshow)
@ -2223,6 +2223,12 @@ viewChatError isCmd logLevel testView = \case
[ withConnEntity
<> "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"
]
SMP _ (SMP.BLOCKED BlockingInfo {reason}) ->
[withConnEntity <> "error: connection blocked by server operator: " <> reasonStr]
where
reasonStr = case reason of
BRSpam -> "spam"
BRContent -> "content violates conditions of use"
BROKER _ NETWORK | not isCmd -> []
BROKER _ TIMEOUT | not isCmd -> []
AGENT A_DUPLICATE -> [withConnEntity <> "error: AGENT A_DUPLICATE" | logLevel == CLLDebug || isCmd]