mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-20 23:13:02 +00:00
core: send badge ZK-proofs to XFTP servers, add file expiry time (#7430)
* core: send badge ZK-proofs to XFTP servers * update simplexmq, use keys and header from simplexmq * core: add file expiry time * refactor * remove posix module * rename migrations * bot api * update UI * import * show item from loaded information * ios: update core library * text * send entitlements in handshake * simplexmq * query plans * update simplexmq --------- Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
This commit is contained in:
co-authored by
Evgeny @ SimpleX Chat
parent
f71b132536
commit
f99c474ca6
@@ -495,9 +495,9 @@ func loadChat(chatId: ChatId, im: ItemsModel, contentTag: MsgContentTag? = nil,
|
||||
)
|
||||
}
|
||||
|
||||
func apiGetChatItemInfo(type: ChatType, id: Int64, scope: GroupChatScope?, itemId: Int64) async throws -> ChatItemInfo {
|
||||
func apiGetChatItemInfo(type: ChatType, id: Int64, scope: GroupChatScope?, itemId: Int64) async throws -> (AChatItem, ChatItemInfo) {
|
||||
let r: ChatResponse0 = try await chatSendCmd(.apiGetChatItemInfo(type: type, id: id, scope: scope, itemId: itemId))
|
||||
if case let .chatItemInfo(_, _, chatItemInfo) = r { return chatItemInfo }
|
||||
if case let .chatItemInfo(_, aci, chatItemInfo) = r { return (aci, chatItemInfo) }
|
||||
throw r.unexpected
|
||||
}
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ struct CIFileView: View {
|
||||
}
|
||||
case let .rcvError(rcvFileError):
|
||||
logger.debug("CIFileView fileAction - in .rcvError")
|
||||
showFileErrorAlert(rcvFileError)
|
||||
showFileErrorAlert(rcvFileError, file)
|
||||
case let .rcvWarning(rcvFileError):
|
||||
logger.debug("CIFileView fileAction - in .rcvWarning")
|
||||
showFileErrorAlert(rcvFileError, temporary: true)
|
||||
@@ -171,10 +171,12 @@ struct CIFileView: View {
|
||||
case .sndError: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
|
||||
case .sndWarning: fileIcon("doc.fill", innerIcon: "exclamationmark.triangle.fill", innerIconSize: 10)
|
||||
case .rcvInvitation:
|
||||
if fileSizeValid(file, senderProfile) {
|
||||
fileIcon("arrow.down.doc.fill", color: theme.colors.primary)
|
||||
} else {
|
||||
if !fileSizeValid(file, senderProfile) {
|
||||
fileIcon("doc.fill", color: .orange, innerIcon: "exclamationmark", innerIconSize: 12)
|
||||
} else if file.expired {
|
||||
fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
|
||||
} else {
|
||||
fileIcon("arrow.down.doc.fill", color: theme.colors.primary)
|
||||
}
|
||||
case .rcvAccepted: fileIcon("doc.fill", innerIcon: "ellipsis", innerIconSize: 12)
|
||||
case let .rcvTransfer(rcvProgress, rcvTotal):
|
||||
@@ -264,7 +266,14 @@ func saveCryptoFile(_ fileSource: CryptoFile) {
|
||||
}
|
||||
}
|
||||
|
||||
func showFileErrorAlert(_ err: FileError, temporary: Bool = false) {
|
||||
func showFileErrorAlert(_ err: FileError, _ file: CIFile? = nil, temporary: Bool = false) {
|
||||
if let file, file.expired, let fileExpires = file.fileExpires, err == .auth || err == .noFile {
|
||||
showAlert(
|
||||
NSLocalizedString("File expired", comment: "file error alert title"),
|
||||
message: String.localizedStringWithFormat(NSLocalizedString("File was available until %@.", comment: "file error text"), localTimestamp(fileExpires))
|
||||
)
|
||||
return
|
||||
}
|
||||
let title: String = if temporary {
|
||||
NSLocalizedString("Temporary file error", comment: "file error alert title")
|
||||
} else {
|
||||
|
||||
@@ -83,7 +83,7 @@ struct CIImageView: View {
|
||||
case .rcvComplete: () // ?
|
||||
case .rcvCancelled: () // TODO
|
||||
case let .rcvError(rcvFileError):
|
||||
showFileErrorAlert(rcvFileError)
|
||||
showFileErrorAlert(rcvFileError, file)
|
||||
case let .rcvWarning(rcvFileError):
|
||||
showFileErrorAlert(rcvFileError, temporary: true)
|
||||
case let .sndError(sndFileError):
|
||||
@@ -152,7 +152,7 @@ struct CIImageView: View {
|
||||
case .sndCancelled: fileIcon("xmark", 10, 13)
|
||||
case .sndError: fileIcon("xmark", 10, 13)
|
||||
case .sndWarning: fileIcon("exclamationmark.triangle.fill", 10, 13)
|
||||
case .rcvInvitation: fileIcon("arrow.down", 10, 13)
|
||||
case .rcvInvitation: fileIcon(file.expired && fileSizeValid(file, senderProfile) ? "xmark" : "arrow.down", 10, 13)
|
||||
case .rcvAccepted: fileIcon("ellipsis", 14, 11)
|
||||
case .rcvTransfer: progressView()
|
||||
case .rcvAborted: fileIcon("exclamationmark.arrow.circlepath", 14, 11)
|
||||
|
||||
@@ -368,7 +368,7 @@ struct CIVideoView: View {
|
||||
.simultaneousGesture(TapGesture().onEnded {
|
||||
showFileErrorAlert(sndFileError, temporary: true)
|
||||
})
|
||||
case .rcvInvitation: fileIcon("arrow.down", 10, 13)
|
||||
case .rcvInvitation: fileIcon(file.expired && fileSizeValid(file, senderProfile) ? "xmark" : "arrow.down", 10, 13)
|
||||
case .rcvAccepted: fileIcon("ellipsis", 14, 11)
|
||||
case let .rcvTransfer(rcvProgress, rcvTotal):
|
||||
if file.fileProtocol == .xftp && rcvProgress < rcvTotal {
|
||||
@@ -382,7 +382,7 @@ struct CIVideoView: View {
|
||||
case let .rcvError(rcvFileError):
|
||||
fileIcon("xmark", 10, 13)
|
||||
.simultaneousGesture(TapGesture().onEnded {
|
||||
showFileErrorAlert(rcvFileError)
|
||||
showFileErrorAlert(rcvFileError, file)
|
||||
})
|
||||
case let .rcvWarning(rcvFileError):
|
||||
fileIcon("exclamationmark.triangle.fill", 10, 13)
|
||||
|
||||
@@ -178,7 +178,7 @@ struct VoiceMessagePlayer: View {
|
||||
.simultaneousGesture(TapGesture().onEnded {
|
||||
showFileErrorAlert(sndFileError, temporary: true)
|
||||
})
|
||||
case .rcvInvitation: downloadButton(recordingFile, "play.fill")
|
||||
case .rcvInvitation: downloadButton(recordingFile, recordingFile.expired ? "multiply" : "play.fill")
|
||||
case .rcvAccepted: loadingIcon()
|
||||
case .rcvTransfer: loadingIcon()
|
||||
case .rcvAborted: downloadButton(recordingFile, "exclamationmark.arrow.circlepath")
|
||||
@@ -187,7 +187,7 @@ struct VoiceMessagePlayer: View {
|
||||
case let .rcvError(rcvFileError):
|
||||
fileStatusIcon("multiply", 14)
|
||||
.simultaneousGesture(TapGesture().onEnded {
|
||||
showFileErrorAlert(rcvFileError)
|
||||
showFileErrorAlert(rcvFileError, recordingFile)
|
||||
})
|
||||
case let .rcvWarning(rcvFileError):
|
||||
fileStatusIcon("exclamationmark.triangle.fill", 16)
|
||||
|
||||
@@ -162,6 +162,9 @@ struct ChatItemInfoView: View {
|
||||
if let deleteAt = meta.itemTimed?.deleteAt {
|
||||
infoRow("Disappears at", localTimestamp(deleteAt))
|
||||
}
|
||||
if let file = ci.file, let fileExpires = file.fileExpires {
|
||||
infoRow(file.expired ? "File was available until" : "File available until", localTimestamp(fileExpires))
|
||||
}
|
||||
if meta.msgVerified?.verified == true {
|
||||
let signedText: LocalizedStringKey = ci.chatDir.sent ? "Signed" : "Signed & verified"
|
||||
HStack {
|
||||
|
||||
@@ -1719,6 +1719,7 @@ struct ChatView: View {
|
||||
@State private var showArchivingReports = false
|
||||
@State private var showChatItemInfoSheet: Bool = false
|
||||
@State private var chatItemInfo: ChatItemInfo?
|
||||
@State private var chatItemInfoItem: ChatItem?
|
||||
@State private var msgWidth: CGFloat = 0
|
||||
@State private var touchInProgress: Bool = false
|
||||
|
||||
@@ -2230,8 +2231,9 @@ struct ChatView: View {
|
||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: alignment)
|
||||
.sheet(isPresented: $showChatItemInfoSheet, onDismiss: {
|
||||
chatItemInfo = nil
|
||||
chatItemInfoItem = nil
|
||||
}) {
|
||||
ChatItemInfoView(ci: ci, userMemberId: chat.chatInfo.groupInfo?.membership.memberId, chatItemInfo: $chatItemInfo)
|
||||
ChatItemInfoView(ci: chatItemInfoItem ?? ci, userMemberId: chat.chatInfo.groupInfo?.membership.memberId, chatItemInfo: $chatItemInfo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2574,8 +2576,9 @@ struct ChatView: View {
|
||||
Task {
|
||||
do {
|
||||
let cInfo = chat.chatInfo
|
||||
let ciInfo = try await apiGetChatItemInfo(type: cInfo.chatType, id: cInfo.apiId, scope: cInfo.groupChatScope(), itemId: ci.id)
|
||||
let (aci, ciInfo) = try await apiGetChatItemInfo(type: cInfo.chatType, id: cInfo.apiId, scope: cInfo.groupChatScope(), itemId: ci.id)
|
||||
await MainActor.run {
|
||||
chatItemInfoItem = aci.chatItem
|
||||
chatItemInfo = ciInfo
|
||||
}
|
||||
if case let .group(gInfo, _) = chat.chatInfo {
|
||||
|
||||
@@ -183,8 +183,8 @@
|
||||
64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; };
|
||||
64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; };
|
||||
64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; };
|
||||
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.3-9pyEF8uuax6HMofQg4cpqD-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.3-9pyEF8uuax6HMofQg4cpqD-ghc9.6.3.a */; };
|
||||
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.3-9pyEF8uuax6HMofQg4cpqD.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.3-9pyEF8uuax6HMofQg4cpqD.a */; };
|
||||
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.3-LojM8yKwVXIArAtVCk2ZRs-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.3-LojM8yKwVXIArAtVCk2ZRs-ghc9.6.3.a */; };
|
||||
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.3-LojM8yKwVXIArAtVCk2ZRs.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.3-LojM8yKwVXIArAtVCk2ZRs.a */; };
|
||||
64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; };
|
||||
64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; };
|
||||
64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; };
|
||||
@@ -563,8 +563,8 @@
|
||||
64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = "<group>"; };
|
||||
64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.3-9pyEF8uuax6HMofQg4cpqD-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.1.0.3-9pyEF8uuax6HMofQg4cpqD-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.3-9pyEF8uuax6HMofQg4cpqD.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.1.0.3-9pyEF8uuax6HMofQg4cpqD.a"; sourceTree = "<group>"; };
|
||||
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.3-LojM8yKwVXIArAtVCk2ZRs-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.1.0.3-LojM8yKwVXIArAtVCk2ZRs-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.3-LojM8yKwVXIArAtVCk2ZRs.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.1.0.3-LojM8yKwVXIArAtVCk2ZRs.a"; sourceTree = "<group>"; };
|
||||
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = "<group>"; };
|
||||
64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = "<group>"; };
|
||||
@@ -735,8 +735,8 @@
|
||||
64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */,
|
||||
64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */,
|
||||
64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */,
|
||||
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.3-9pyEF8uuax6HMofQg4cpqD-ghc9.6.3.a in Frameworks */,
|
||||
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.3-9pyEF8uuax6HMofQg4cpqD.a in Frameworks */,
|
||||
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.3-LojM8yKwVXIArAtVCk2ZRs-ghc9.6.3.a in Frameworks */,
|
||||
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.3-LojM8yKwVXIArAtVCk2ZRs.a in Frameworks */,
|
||||
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -822,8 +822,8 @@
|
||||
64C829992D54AEEE006B9E89 /* libffi.a */,
|
||||
64C829982D54AEED006B9E89 /* libgmp.a */,
|
||||
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */,
|
||||
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.3-9pyEF8uuax6HMofQg4cpqD-ghc9.6.3.a */,
|
||||
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.3-9pyEF8uuax6HMofQg4cpqD.a */,
|
||||
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.3-LojM8yKwVXIArAtVCk2ZRs-ghc9.6.3.a */,
|
||||
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.3-LojM8yKwVXIArAtVCk2ZRs.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
|
||||
@@ -4685,6 +4685,7 @@ public struct CIFile: Decodable, Hashable {
|
||||
public var fileSource: CryptoFile?
|
||||
public var fileStatus: CIFileStatus
|
||||
public var fileProtocol: FileProtocol
|
||||
public var fileExpires: Date? = nil
|
||||
|
||||
public static func getSample(fileId: Int64 = 1, fileName: String = "test.txt", fileSize: Int64 = 100, filePath: String? = "test.txt", fileStatus: CIFileStatus = .rcvComplete) -> CIFile {
|
||||
let f: CryptoFile?
|
||||
@@ -4718,6 +4719,10 @@ public struct CIFile: Decodable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
public var expired: Bool {
|
||||
if let fileExpires { fileExpires < Date.now } else { false }
|
||||
}
|
||||
|
||||
public var cancelAction: CancelAction? {
|
||||
get {
|
||||
switch self.fileStatus {
|
||||
@@ -4754,7 +4759,7 @@ public struct CIFile: Decodable, Hashable {
|
||||
case .sndCancelled: true
|
||||
case .sndError: true
|
||||
case .sndWarning: true
|
||||
case .rcvInvitation: false
|
||||
case .rcvInvitation: expired
|
||||
case .rcvAccepted: true
|
||||
case .rcvTransfer: true
|
||||
case .rcvAborted: true
|
||||
|
||||
+5
-2
@@ -4252,8 +4252,11 @@ data class CIFile(
|
||||
val fileSize: Long,
|
||||
val fileSource: CryptoFile? = null,
|
||||
val fileStatus: CIFileStatus,
|
||||
val fileProtocol: FileProtocol
|
||||
val fileProtocol: FileProtocol,
|
||||
val fileExpires: Instant? = null
|
||||
) {
|
||||
val expired: Boolean = fileExpires != null && fileExpires < Clock.System.now()
|
||||
|
||||
val loaded: Boolean = when (fileStatus) {
|
||||
is CIFileStatus.SndStored -> true
|
||||
is CIFileStatus.SndTransfer -> true
|
||||
@@ -4303,7 +4306,7 @@ data class CIFile(
|
||||
is CIFileStatus.SndCancelled -> true
|
||||
is CIFileStatus.SndError -> true
|
||||
is CIFileStatus.SndWarning -> true
|
||||
is CIFileStatus.RcvInvitation -> false
|
||||
is CIFileStatus.RcvInvitation -> expired
|
||||
is CIFileStatus.RcvAccepted -> true
|
||||
is CIFileStatus.RcvTransfer -> true
|
||||
is CIFileStatus.RcvAborted -> true
|
||||
|
||||
+2
-2
@@ -1161,9 +1161,9 @@ object ChatController {
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiGetChatItemInfo(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, itemId: Long): ChatItemInfo? {
|
||||
suspend fun apiGetChatItemInfo(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, itemId: Long): Pair<ChatItem, ChatItemInfo>? {
|
||||
val r = sendCmd(rh, CC.ApiGetChatItemInfo(type, id, scope, itemId))
|
||||
if (r is API.Result && r.res is CR.ApiChatItemInfo) return r.res.chatItemInfo
|
||||
if (r is API.Result && r.res is CR.ApiChatItemInfo) return r.res.chatItem.chatItem to r.res.chatItemInfo
|
||||
apiErrorAlert("apiGetChatItemInfo", generalGetString(MR.strings.error_loading_details), r)
|
||||
return null
|
||||
}
|
||||
|
||||
+5
@@ -273,6 +273,11 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
if (deleteAt != null) {
|
||||
InfoRow(stringResource(MR.strings.info_row_disappears_at), localTimestamp(deleteAt))
|
||||
}
|
||||
val file = ci.file
|
||||
if (file?.fileExpires != null) {
|
||||
val expiresRes = if (file.expired) MR.strings.info_row_file_expired else MR.strings.info_row_file_expires
|
||||
InfoRow(stringResource(expiresRes), localTimestamp(file.fileExpires))
|
||||
}
|
||||
if (ci.meta.msgVerified?.verified == true) {
|
||||
val signedRes = if (sent) MR.strings.info_row_signed else MR.strings.info_row_signed_verified
|
||||
InfoRow(stringResource(signedRes), "", icon = painterResource(MR.images.ic_verified))
|
||||
|
||||
+4
-3
@@ -699,7 +699,7 @@ fun ChatView(
|
||||
}
|
||||
},
|
||||
showItemDetails = { cInfo, cItem ->
|
||||
suspend fun loadChatItemInfo(): ChatItemInfo? = coroutineScope {
|
||||
suspend fun loadChatItemInfo(): Pair<ChatItem, ChatItemInfo>? = coroutineScope {
|
||||
val ciInfo = chatModel.controller.apiGetChatItemInfo(chatRh, cInfo.chatType, cInfo.apiId, cInfo.groupChatScope(), cItem.id)
|
||||
if (ciInfo != null) {
|
||||
if (chatInfo is ChatInfo.Group) {
|
||||
@@ -717,11 +717,12 @@ fun ChatView(
|
||||
}
|
||||
ModalManager.end.showModalCloseable(endButtons = {
|
||||
ShareButton {
|
||||
clipboard.shareText(itemInfoShareText(chatModel, cItem, initialCiInfo, chatModel.controller.appPrefs.developerTools.get()))
|
||||
clipboard.shareText(itemInfoShareText(chatModel, initialCiInfo.first, initialCiInfo.second, chatModel.controller.appPrefs.developerTools.get()))
|
||||
}
|
||||
}) { close ->
|
||||
var ciInfo by remember(cItem.id) { mutableStateOf(initialCiInfo) }
|
||||
ChatItemInfoView(chatRh, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get(), chatInfo)
|
||||
val (item, info) = ciInfo
|
||||
ChatItemInfoView(chatRh, item, info, devTools = chatModel.controller.appPrefs.developerTools.get(), chatInfo)
|
||||
LaunchedEffect(cItem.id) {
|
||||
withContext(Dispatchers.Default) {
|
||||
for (msg in controller.messagesChannel) {
|
||||
|
||||
+15
-5
@@ -101,7 +101,7 @@ fun CIFileView(
|
||||
FileProtocol.LOCAL -> {}
|
||||
}
|
||||
file.fileStatus is CIFileStatus.RcvError ->
|
||||
showFileErrorAlert(file.fileStatus.rcvFileError)
|
||||
showFileErrorAlert(file.fileStatus.rcvFileError, file)
|
||||
file.fileStatus is CIFileStatus.RcvWarning ->
|
||||
showFileErrorAlert(file.fileStatus.rcvFileError, temporary = true)
|
||||
file.fileStatus is CIFileStatus.SndError ->
|
||||
@@ -157,10 +157,12 @@ fun CIFileView(
|
||||
is CIFileStatus.SndError -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
|
||||
is CIFileStatus.SndWarning -> fileIcon(innerIcon = painterResource(MR.images.ic_warning_filled))
|
||||
is CIFileStatus.RcvInvitation ->
|
||||
if (fileSizeValid(file, senderProfile))
|
||||
fileIcon(innerIcon = painterResource(MR.images.ic_arrow_downward), color = MaterialTheme.colors.primary, topPadding = 10.sp.toDp())
|
||||
else
|
||||
if (!fileSizeValid(file, senderProfile))
|
||||
fileIcon(innerIcon = painterResource(MR.images.ic_priority_high), color = WarningOrange)
|
||||
else if (file.expired)
|
||||
fileIcon(innerIcon = painterResource(MR.images.ic_close))
|
||||
else
|
||||
fileIcon(innerIcon = painterResource(MR.images.ic_arrow_downward), color = MaterialTheme.colors.primary, topPadding = 10.sp.toDp())
|
||||
is CIFileStatus.RcvAccepted -> fileIcon(innerIcon = painterResource(MR.images.ic_more_horiz))
|
||||
is CIFileStatus.RcvTransfer ->
|
||||
if (file.fileProtocol == FileProtocol.XFTP && file.fileStatus.rcvProgress < file.fileStatus.rcvTotal) {
|
||||
@@ -241,7 +243,15 @@ fun CIFileView(
|
||||
fun fileSizeValid(file: CIFile, senderProfile: LocalProfile?): Boolean =
|
||||
file.fileSize <= getMaxFileSize(file.fileProtocol, senderProfile)
|
||||
|
||||
fun showFileErrorAlert(err: FileError, temporary: Boolean = false) {
|
||||
fun showFileErrorAlert(err: FileError, file: CIFile? = null, temporary: Boolean = false) {
|
||||
val fileExpires = file?.fileExpires
|
||||
if (file != null && fileExpires != null && file.expired && (err is FileError.Auth || err is FileError.NoFile)) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.file_expired),
|
||||
String.format(generalGetString(MR.strings.file_error_expired), localTimestamp(fileExpires))
|
||||
)
|
||||
return
|
||||
}
|
||||
val title: String = generalGetString(if (temporary) MR.strings.temporary_file_error else MR.strings.file_error)
|
||||
val btn = err.moreInfoButton
|
||||
if (btn != null) {
|
||||
|
||||
+6
-2
@@ -83,7 +83,11 @@ fun CIImageView(
|
||||
is CIFileStatus.SndCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||
is CIFileStatus.SndError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||
is CIFileStatus.SndWarning -> fileIcon(painterResource(MR.images.ic_warning_filled), MR.strings.icon_descr_file)
|
||||
is CIFileStatus.RcvInvitation -> fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_asked_to_receive)
|
||||
is CIFileStatus.RcvInvitation ->
|
||||
if (file.expired && fileSizeValid(file, senderProfile))
|
||||
fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||
else
|
||||
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.RcvTransfer -> progressIndicator()
|
||||
is CIFileStatus.RcvComplete -> {}
|
||||
@@ -239,7 +243,7 @@ fun CIImageView(
|
||||
FileProtocol.LOCAL -> {}
|
||||
}
|
||||
file.fileStatus is CIFileStatus.RcvError ->
|
||||
showFileErrorAlert(file.fileStatus.rcvFileError)
|
||||
showFileErrorAlert(file.fileStatus.rcvFileError, file)
|
||||
file.fileStatus is CIFileStatus.RcvWarning ->
|
||||
showFileErrorAlert(file.fileStatus.rcvFileError, temporary = true)
|
||||
file.fileStatus is CIFileStatus.SndError ->
|
||||
|
||||
+9
-5
@@ -135,10 +135,10 @@ fun CIVideoView(
|
||||
}
|
||||
// Do not show download icon when the view is blurred
|
||||
if (!smallView && (!showDownloadButton(file?.fileStatus) || !blurred.value)) {
|
||||
fileStatusIcon(file, false)
|
||||
fileStatusIcon(file, false, senderProfile)
|
||||
} else if (smallView && file?.showStatusIconInSmallView == true) {
|
||||
Box(Modifier.align(Alignment.Center)) {
|
||||
fileStatusIcon(file, true)
|
||||
fileStatusIcon(file, true, senderProfile)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -486,7 +486,7 @@ private fun progressCircle(progress: Long, total: Long) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun fileStatusIcon(file: CIFile?, smallView: Boolean) {
|
||||
private fun fileStatusIcon(file: CIFile?, smallView: Boolean, senderProfile: LocalProfile?) {
|
||||
if (file != null) {
|
||||
Box(
|
||||
Modifier
|
||||
@@ -525,7 +525,11 @@ private fun fileStatusIcon(file: CIFile?, smallView: Boolean) {
|
||||
showFileErrorAlert(file.fileStatus.sndFileError, temporary = true)
|
||||
}
|
||||
)
|
||||
is CIFileStatus.RcvInvitation -> fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_video_asked_to_receive)
|
||||
is CIFileStatus.RcvInvitation ->
|
||||
if (file.expired && fileSizeValid(file, senderProfile))
|
||||
fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||
else
|
||||
fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_video_asked_to_receive)
|
||||
is CIFileStatus.RcvAccepted -> fileIcon(painterResource(MR.images.ic_more_horiz), MR.strings.icon_descr_waiting_for_video)
|
||||
is CIFileStatus.RcvTransfer ->
|
||||
if (file.fileProtocol == FileProtocol.XFTP && file.fileStatus.rcvProgress < file.fileStatus.rcvTotal) {
|
||||
@@ -541,7 +545,7 @@ private fun fileStatusIcon(file: CIFile?, smallView: Boolean) {
|
||||
painterResource(MR.images.ic_close),
|
||||
MR.strings.icon_descr_file,
|
||||
onClick = {
|
||||
showFileErrorAlert(file.fileStatus.rcvFileError)
|
||||
showFileErrorAlert(file.fileStatus.rcvFileError, file)
|
||||
}
|
||||
)
|
||||
is CIFileStatus.RcvWarning ->
|
||||
|
||||
+2
-2
@@ -412,7 +412,7 @@ private fun VoiceMsgIndicator(
|
||||
}
|
||||
)
|
||||
file?.fileStatus is CIFileStatus.RcvInvitation ->
|
||||
PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, sizeMultiplier, { receiveFile(file.fileId) }, {}, longClick = longClick)
|
||||
PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, sizeMultiplier, { receiveFile(file.fileId) }, {}, longClick = longClick, icon = if (file.expired) MR.images.ic_close else MR.images.ic_play_arrow_filled)
|
||||
file?.fileStatus is CIFileStatus.RcvTransfer || file?.fileStatus is CIFileStatus.RcvAccepted ->
|
||||
VoiceMsgLoadingProgressIndicator(sizeMultiplier)
|
||||
file?.fileStatus is CIFileStatus.RcvAborted ->
|
||||
@@ -424,7 +424,7 @@ private fun VoiceMsgIndicator(
|
||||
sizeMultiplier,
|
||||
longClick,
|
||||
onClick = {
|
||||
showFileErrorAlert(file.fileStatus.rcvFileError)
|
||||
showFileErrorAlert(file.fileStatus.rcvFileError, file)
|
||||
}
|
||||
)
|
||||
file != null && file.fileStatus is CIFileStatus.RcvWarning ->
|
||||
|
||||
@@ -697,6 +697,8 @@
|
||||
<string name="loading_remote_file_title">Loading the file </string>
|
||||
<string name="loading_remote_file_desc">Please, wait while the file is being loaded from the linked mobile</string>
|
||||
<string name="file_error">File error</string>
|
||||
<string name="file_expired">File expired</string>
|
||||
<string name="file_error_expired">File was available until %1$s.</string>
|
||||
<string name="temporary_file_error">Temporary file error</string>
|
||||
<string name="open_with_app">Open with %s</string>
|
||||
|
||||
@@ -2044,6 +2046,8 @@
|
||||
<string name="info_row_deleted_at">Deleted at</string>
|
||||
<string name="info_row_moderated_at">Moderated at</string>
|
||||
<string name="info_row_disappears_at">Disappears at</string>
|
||||
<string name="info_row_file_expires">File available until</string>
|
||||
<string name="info_row_file_expired">File was available until</string>
|
||||
<string name="share_text_database_id">Database ID: %d</string>
|
||||
<string name="share_text_updated_at">Record updated at: %s</string>
|
||||
<string name="share_text_message_status">Message status: %s</string>
|
||||
|
||||
@@ -747,6 +747,7 @@ LocalRcv:
|
||||
- fileSource: [CryptoFile](#cryptofile)?
|
||||
- fileStatus: [CIFileStatus](#cifilestatus)
|
||||
- fileProtocol: [FileProtocol](#fileprotocol)
|
||||
- fileExpires: UTCTime?
|
||||
|
||||
|
||||
---
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: 0d3cf39c27aec1c51c88d7bf3d6762bc22278967
|
||||
tag: 1876357c7f64fc00d9d031084c5e78e844b27c34
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
||||
@@ -692,6 +692,7 @@ export interface CIFile {
|
||||
fileSource?: CryptoFile
|
||||
fileStatus: CIFileStatus
|
||||
fileProtocol: FileProtocol
|
||||
fileExpires?: string // ISO-8601 timestamp
|
||||
}
|
||||
|
||||
export type CIFileStatus =
|
||||
|
||||
@@ -481,6 +481,7 @@ class CIFile(TypedDict):
|
||||
fileSource: NotRequired["CryptoFile"]
|
||||
fileStatus: "CIFileStatus"
|
||||
fileProtocol: "FileProtocol"
|
||||
fileExpires: NotRequired[str] # ISO-8601 timestamp
|
||||
|
||||
class CIFileStatus_sndStored(TypedDict):
|
||||
type: Literal["sndStored"]
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
# File expiry display
|
||||
|
||||
Show the sender and recipients when an XFTP file stops being downloadable.
|
||||
|
||||
The value is the storage expiry the server grants. The agent already reports it in `SFDONE` as `Maybe GrantedStorageTime` (`GSTExpires { epochSeconds }`, absolute UTC). No simplexmq change. `Nothing` means unknown (a server or chunk below the storage-time version).
|
||||
|
||||
## Protocol
|
||||
|
||||
In `Simplex.Chat.Protocol`:
|
||||
|
||||
- add optional `fileExpires :: Maybe UTCTime` to `XMsgFileDescr`
|
||||
- encode with `"fileExpires" .=? fileExpires`, decode with `opt "fileExpires"`
|
||||
- no chat version bump; an older app skips the field, an older sender omits it
|
||||
|
||||
## Chat item file
|
||||
|
||||
In `Simplex.Chat.Messages`:
|
||||
|
||||
- add `fileExpires :: Maybe UTCTime` to `CIFile`
|
||||
- add no `CIFileStatus`; the app derives the expired state from `fileExpires < now`
|
||||
|
||||
## Store
|
||||
|
||||
In the SQLite and PostgreSQL stores:
|
||||
|
||||
- add a nullable `file_expires_at` column to `files`, typed as the other `files` timestamps; migration in both stores
|
||||
- add `setFileExpires :: DB.Connection -> User -> FileTransferId -> UTCTime -> IO ()`
|
||||
- read `file_expires_at` into `CIFile` in the file-row queries
|
||||
|
||||
## Sender
|
||||
|
||||
In `Simplex.Chat.Library.Subscriber`, `SFDONE` handler:
|
||||
|
||||
- bind the granted time (currently dropped)
|
||||
- convert `GSTExpires epochSeconds` to `UTCTime` and store it with `setFileExpires`
|
||||
- thread the expiry through `sendFileDescriptions` into each `XMsgFileDescr` it builds
|
||||
|
||||
## Recipient
|
||||
|
||||
In `Simplex.Chat.Library.Subscriber`, `XMsgFileDescr` handling (direct and group):
|
||||
|
||||
- take the received `fileExpires` and store it with `setFileExpires`
|
||||
|
||||
## Apps
|
||||
|
||||
Received files show the expiry; sent files are unchanged, they keep the checkmark. A tap still attempts the download: the receive actions, the download overlays, and the "Download file" menu item stay as they are.
|
||||
|
||||
Model:
|
||||
|
||||
- `CIFile` gets `fileExpires` — `Date?` in `SimpleXChat/ChatTypes.swift`, `Instant?` in `model/ChatModel.kt`
|
||||
- `CIFile` gets `expired`, beside `loaded`: `fileExpires` is set and has passed
|
||||
|
||||
Message information, in `ChatItemInfoView`, a row after "Disappears at":
|
||||
|
||||
- "File can be received until <time>", or "File was available until <time>" when expired
|
||||
|
||||
File indicator, the `rcvInvitation` icon in each view — `CIFileView`, `CIImageView`, `CIVideoView`, `CIVoiceView` on both platforms:
|
||||
|
||||
- when expired, show the X that view already uses for `rcvError`, in place of the download arrow (`play.fill` for voice)
|
||||
- `showStatusIconInSmallView` returns `expired` for `rcvInvitation`, so a small view shows the X too
|
||||
|
||||
Alert, in `showFileErrorAlert`:
|
||||
|
||||
- the function takes the expiry
|
||||
- when the file expired and the error is `auth` or `noFile`, the alert reads "File expired" / "File was available until <time>", in place of "Wrong key or unknown file chunk address…" and "File not found…"
|
||||
|
||||
Strings:
|
||||
|
||||
- `strings.xml`: `info_row_file_expires`, `info_row_file_expired`, `file_expired`, `file_error_expired`
|
||||
- iOS takes the literals in the source, `en.lproj/Localizable.strings` holds overrides only
|
||||
|
||||
Left out: promotion of badges for longer storage, and for the file size limit in the two "Large file!" alerts. The apps have no badge screen; the only badge link is "Learn more" in `showBadgeInfoAlert`.
|
||||
|
||||
An expired file that also exceeds the size limit for the sender's badge keeps the size behaviour: the icon stays the size warning, and the tap reports the size limit. The expiry applies only to files within the limit.
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"https://github.com/simplex-chat/simplexmq.git"."0d3cf39c27aec1c51c88d7bf3d6762bc22278967" = "115m1h8pc4wdbd5y1prz7qgqnxhsin1f6m53k4szab9c1iawlf4v";
|
||||
"https://github.com/simplex-chat/simplexmq.git"."1876357c7f64fc00d9d031084c5e78e844b27c34" = "0f8jmw4xgzralhin0x6n67rk1ss7kq5n744g22vgy9lwn8x4wfir";
|
||||
"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";
|
||||
|
||||
@@ -155,6 +155,7 @@ library
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20260723_contact_request_rejection
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20260813_auto_accept_group_invitations
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20260822_forward_link
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20260828_file_expiry
|
||||
else
|
||||
exposed-modules:
|
||||
Simplex.Chat.Archive
|
||||
@@ -327,6 +328,7 @@ library
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20260723_contact_request_rejection
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20260813_auto_accept_group_invitations
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20260822_forward_link
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20260828_file_expiry
|
||||
other-modules:
|
||||
Paths_simplex_chat
|
||||
hs-source-dirs:
|
||||
|
||||
+5
-13
@@ -28,8 +28,8 @@ import qualified Data.Map.Strict as M
|
||||
import Data.Maybe (fromMaybe, mapMaybe)
|
||||
import Data.Text (Text)
|
||||
import Data.Time.Clock (getCurrentTime, nominalDay)
|
||||
import Simplex.Chat.Badges (badgeServerCredential)
|
||||
import Simplex.Chat.Controller
|
||||
import Simplex.Chat.Badges (BBSPublicKeyStr (..))
|
||||
import Simplex.Chat.Library.Commands
|
||||
import Simplex.Chat.Operators
|
||||
import Simplex.Chat.Operators.Presets
|
||||
@@ -52,6 +52,7 @@ import Simplex.Messaging.Agent.Store.Entity
|
||||
import Simplex.Messaging.Agent.Store.Shared (MigrationConfig (..), MigrationConfirmation (..), MigrationError)
|
||||
import Simplex.Messaging.Client (defaultNetworkConfig)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.Entitlement (entitlementIssuerKeys)
|
||||
import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), ProtocolType (..), SProtocolType (..), SubscriptionMode (..), UserProtocol)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import qualified UnliftIO.Exception as E
|
||||
@@ -66,17 +67,7 @@ defaultChatConfig =
|
||||
tbqSize = 1024
|
||||
},
|
||||
chatVRange = supportedChatVRange,
|
||||
badgePublicKeys =
|
||||
M.fromList
|
||||
[ (1, toBBSPublicKey "mW_5Zp1wHnXDF56wOZwFcRjGrf0GLLsfyymIQDqYoWfjfvS7oQWSfi7hH65N8JhuE9x8wbKXHidnQLO4GnOSMP_bRKUMH1qIzv5SQKFHNM8G4PaWcTcri8iZLc-3xhSI"),
|
||||
(2, toBBSPublicKey "odGCB7uVDXTURsHgSvSciByV4Q3-3ZvEB8myDsDJqm-PwOYc5-At36uc7n_pyUDxEQEHr9i4RJgFih2FSArPW-EQBXNPNf4wTtA0znn74qLEGc4fh9pVYPEIm_ZGbnsJ"),
|
||||
(3, toBBSPublicKey "txkT2003WMjc43KvYvPKEcR970NLmw5UZY51eUqgk91sgp53idt1HTlKYvnrEttJDFMlctYf1-bpri0e9DhBQ-xk1J4WoLN2uif_1OcA1pGCobpk9lwtsq1Idek4biy0"),
|
||||
(4, toBBSPublicKey "q_YzegihaLYrEm9z3cAghsfDGNZfXuEpQGMJERJQS4M0Szl4gvSC_fV_muKc3NIMA_8iYuBN8qyvb5U55RctCRn3kleFQ4sqf-WBgoydX6UVo7BsYcUbXWWEFZXlOGIH"),
|
||||
(5, toBBSPublicKey "oqymHASH_okefShrnz4HnTooUNlE1WoDRnSrgd0bTCpOacgJWBsMpwZpdmYlX-vQAKAC_zmI4VdKoOznnhW-sdUXZw6bthCi5JYjGxCR1Co27i1tix5UXCTbR5Jp901-"),
|
||||
(6, toBBSPublicKey "kDqaB6zKSRp_97QPFj5JPDlo0vzfSTLSp9goFx1qajv4q4H6dR6BbkmWZ4xx_9Q2AxmcpqcV0ethz1OH-Jk_Sz2J1mIz1PUVM9LkdLhi_PNtqhezzO5dbVs-HJ1fNqe6"),
|
||||
(7, toBBSPublicKey "rl36D5mg2N3NmmEybxE_RBeU9YZ_zeXNPfp7ZMLtUEuf2Mo4OQM_Up1v5rX_IqICD-AIJcuyptEBsELx_PJQzpmiNuG5I4cWO6HkRKtc6fVFvgZMrDJjaascPd1CIyxX"),
|
||||
(8, toBBSPublicKey "joM3Bnt7JPt5JiwQwERHGjro2iVZ0mPD_clUh4hzkhxvbjuFrWuTmfSNA8PWBqGKEGNl13aRi1pMf6yY14E27c5C71JxWm7T-rZaBrGPEUWifhD-qidWuf3PU7KJCCWd")
|
||||
],
|
||||
badgePublicKeys = M.mapKeys fromIntegral entitlementIssuerKeys,
|
||||
confirmMigrations = MCConsole,
|
||||
-- this property should NOT use operator = Nothing
|
||||
-- non-operator servers can be passed via options
|
||||
@@ -292,7 +283,8 @@ newChatController
|
||||
let opDomains = operatorDomains $ mapMaybe snd ops
|
||||
(smp', xftp') <- unzip <$> mapM (getServers ops opDomains) users
|
||||
let useServices = M.fromList $ map (\User {agentUserId = AgentUserId uId, clientService} -> (uId, isTrue clientService)) users
|
||||
pure InitialAgentServers {smp = M.fromList (optServers smp' smpServers), xftp = M.fromList (optServers xftp' xftpServers), ntf, netCfg, useServices, presetDomains, presetServers = L.toList allPresetServers}
|
||||
entitlements = M.fromList $ mapMaybe (\User {agentUserId = AgentUserId uId, profile = LocalProfile {localBadge}} -> (uId,) <$> badgeServerCredential localBadge) users
|
||||
pure InitialAgentServers {smp = M.fromList (optServers smp' smpServers), xftp = M.fromList (optServers xftp' xftpServers), entitlements, ntf, netCfg, useServices, presetDomains, presetServers = L.toList allPresetServers}
|
||||
where
|
||||
optServers :: [(UserId, NonEmpty (ServerCfg p))] -> [ProtoServerWithAuth p] -> [(UserId, NonEmpty (ServerCfg p))]
|
||||
optServers srvs overrides_ = case L.nonEmpty overrides_ of
|
||||
|
||||
@@ -26,6 +26,7 @@ module Simplex.Chat.Badges
|
||||
localBadgeInfo,
|
||||
localBadgeStatus,
|
||||
maxXFTPFileSize,
|
||||
badgeServerCredential,
|
||||
maxFileSizeSupporter,
|
||||
maxFileSizeLegend,
|
||||
ProofPresHeaderTag (..),
|
||||
@@ -69,6 +70,7 @@ import Simplex.FileTransfer.Description (gb, maxFileSize)
|
||||
import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..), fromTextField_)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.BBS
|
||||
import Simplex.Messaging.Crypto.Entitlement (Entitlement (Entitlement), EntitlementCredential (EntitlementCredential), MasterKey (MasterKey), entitlementBBSHeader)
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON)
|
||||
#if defined(dbPostgres)
|
||||
@@ -187,6 +189,12 @@ maxFileSizeSupporter = gb 2
|
||||
maxFileSizeLegend :: Int64
|
||||
maxFileSizeLegend = gb 5
|
||||
|
||||
badgeServerCredential :: Maybe LocalBadge -> Maybe EntitlementCredential
|
||||
badgeServerCredential = \case
|
||||
Just (OwnBadge (BadgeCredential idx (BadgeMasterKey mk) sig BadgeInfo {badgeType, badgeExpiry, badgeExtra}) _) ->
|
||||
(\e -> EntitlementCredential (fromIntegral idx) (MasterKey mk) (Entitlement e (textEncode badgeType) badgeExtra) sig) <$> badgeExpiry
|
||||
_ -> Nothing
|
||||
|
||||
maxXFTPFileSize :: Maybe LocalBadge -> Int64
|
||||
maxXFTPFileSize = \case
|
||||
Just b | localBadgeStatus b == BSActive -> case badgeType (localBadgeInfo b) of
|
||||
@@ -269,7 +277,7 @@ newtype VerifiedBadgeRequest = VerifiedBadgeRequest BadgeRequest
|
||||
-- Constants
|
||||
|
||||
bbsBadgeHeader :: BBSHeader
|
||||
bbsBadgeHeader = BBSHeader "SimpleX badges v1"
|
||||
bbsBadgeHeader = entitlementBBSHeader
|
||||
|
||||
bbsBadgeMessageCount :: Int
|
||||
bbsBadgeMessageCount = 4
|
||||
|
||||
@@ -56,7 +56,7 @@ import Data.Type.Equality
|
||||
import qualified Data.UUID as UUID
|
||||
import qualified Data.UUID.V4 as V4
|
||||
import Simplex.Chat.Library.Subscriber
|
||||
import Simplex.Chat.Badges (BadgeCredential (..), LocalBadge (..), maxXFTPFileSize, mkBadgeStatus, verifyCredential)
|
||||
import Simplex.Chat.Badges (BadgeCredential (..), LocalBadge (..), badgeServerCredential, maxXFTPFileSize, mkBadgeStatus, verifyCredential)
|
||||
import Simplex.Chat.Names (SimplexDomainProof (..), SimplexDomainClaim (..), claimDomain, mkDomainClaim)
|
||||
import Simplex.Chat.Call
|
||||
import Simplex.Chat.Controller
|
||||
@@ -5000,7 +5000,7 @@ processChatCommand cxt nm = \case
|
||||
chunkSize <- asks $ fileChunkSize . config
|
||||
withFastStore' $ \db -> do
|
||||
fileId <- createLocalFile CIFSSndStored db user nf createdAt cf fileSize chunkSize
|
||||
pure CIFile {fileId, fileName = takeFileName filePath, fileSize, fileSource = Just cf, fileStatus = CIFSSndStored, fileProtocol = FPLocal}
|
||||
pure CIFile {fileId, fileName = takeFileName filePath, fileSize, fileSource = Just cf, fileStatus = CIFSSndStored, fileProtocol = FPLocal, fileExpires = Nothing}
|
||||
prepareLocalItemsData ::
|
||||
NonEmpty ComposedMessageReq ->
|
||||
NonEmpty (Maybe (CIFile 'MDSnd)) ->
|
||||
@@ -5142,8 +5142,10 @@ addUserBadge user cred@(BadgeCredential keyIdx _ _ info) = do
|
||||
verified <- liftIO $ verifyCredential key cred
|
||||
unless verified $ throwCmdError "badge credential does not verify against configured key"
|
||||
now <- liftIO getCurrentTime
|
||||
user' <- withFastStore' $ \db -> setUserBadge db user (Just (OwnBadge cred (mkBadgeStatus now (Just True) info)))
|
||||
let badge = Just (OwnBadge cred (mkBadgeStatus now (Just True) info))
|
||||
user' <- withFastStore' $ \db -> setUserBadge db user badge
|
||||
asks currentUser >>= atomically . (`writeTVar` Just user')
|
||||
lift $ withAgent' $ \a -> setUserEntitlement a (aUserId user') (badgeServerCredential badge)
|
||||
cxt <- asks $ mkStoreCxt . config
|
||||
contacts <- withFastStore' $ \db -> getUserContacts db cxt user'
|
||||
withChatLock "addUserBadge" $ forM_ contacts $ \ct ->
|
||||
|
||||
@@ -441,12 +441,12 @@ xftpSndFileTransfer_ user file@(CryptoFile filePath cfArgs) fileSize n contactOr
|
||||
fInv = xftpFileInvitation fileName fileSize dummyFileDescr
|
||||
fsFilePath <- lift $ toFSFilePath filePath
|
||||
let srcFile = CryptoFile fsFilePath cfArgs
|
||||
aFileId <- withAgent $ \a -> xftpSendFile a (aUserId user) srcFile (roundedFDCount n)
|
||||
aFileId <- withAgent $ \a -> xftpSendFile a (aUserId user) srcFile (roundedFDCount n) Nothing
|
||||
-- TODO CRSndFileStart event for XFTP
|
||||
chSize <- asks $ fileChunkSize . config
|
||||
ft@FileTransferMeta {fileId} <- withStore' $ \db -> createSndFileTransferXFTP db user contactOrGroup_ file fInv (AgentSndFileId aFileId) Nothing chSize
|
||||
let fileSource = Just $ CryptoFile filePath cfArgs
|
||||
ciFile = CIFile {fileId, fileName, fileSize, fileSource, fileStatus = CIFSSndStored, fileProtocol = FPXFTP}
|
||||
ciFile = CIFile {fileId, fileName, fileSize, fileSource, fileStatus = CIFSSndStored, fileProtocol = FPXFTP, fileExpires = Nothing}
|
||||
pure (fInv, ciFile, ft)
|
||||
|
||||
cryptoFileDigest :: CryptoFile -> CM FD.FileDigest
|
||||
@@ -1417,7 +1417,7 @@ sendHistory user gInfo@GroupInfo {membership} m@GroupMember {activeConn = Just c
|
||||
resolveAuthor (Just gmId) = do
|
||||
cxt <- chatStoreCxt
|
||||
eitherToMaybe <$> withStore' (\db -> runExceptT $ getGroupMemberById db cxt user gmId)
|
||||
getRcvFileInvDescr :: CIFile 'MDRcv -> CM (Maybe (FileInvitation, RcvFileDescrText))
|
||||
getRcvFileInvDescr :: CIFile 'MDRcv -> CM (Maybe (FileInvitation, RcvFileDescrText, Maybe UTCTime))
|
||||
getRcvFileInvDescr ciFile@CIFile {fileId, fileProtocol, fileStatus} = do
|
||||
expired <- fileExpired
|
||||
if fileProtocol /= FPXFTP || fileStatus == CIFSRcvCancelled || expired
|
||||
@@ -1425,7 +1425,7 @@ sendHistory user gInfo@GroupInfo {membership} m@GroupMember {activeConn = Just c
|
||||
else do
|
||||
rfd <- withStore $ \db -> getRcvFileDescrByRcvFileId db fileId
|
||||
pure $ invCompleteDescr ciFile rfd
|
||||
getSndFileInvDescr :: CIFile 'MDSnd -> CM (Maybe (FileInvitation, RcvFileDescrText))
|
||||
getSndFileInvDescr :: CIFile 'MDSnd -> CM (Maybe (FileInvitation, RcvFileDescrText, Maybe UTCTime))
|
||||
getSndFileInvDescr ciFile@CIFile {fileId, fileProtocol, fileStatus} = do
|
||||
expired <- fileExpired
|
||||
if fileProtocol /= FPXFTP || fileStatus == CIFSSndCancelled || expired
|
||||
@@ -1440,21 +1440,21 @@ sendHistory user gInfo@GroupInfo {membership} m@GroupMember {activeConn = Just c
|
||||
ttl <- asks $ rcvFilesTTL . agentConfig . config
|
||||
cutoffTs <- addUTCTime (-ttl) <$> liftIO getCurrentTime
|
||||
pure $ chatItemTs cci < cutoffTs
|
||||
invCompleteDescr :: CIFile d -> RcvFileDescr -> Maybe (FileInvitation, RcvFileDescrText)
|
||||
invCompleteDescr CIFile {fileName, fileSize} RcvFileDescr {fileDescrText, fileDescrComplete}
|
||||
invCompleteDescr :: CIFile d -> RcvFileDescr -> Maybe (FileInvitation, RcvFileDescrText, Maybe UTCTime)
|
||||
invCompleteDescr CIFile {fileName, fileSize, fileExpires} RcvFileDescr {fileDescrText, fileDescrComplete}
|
||||
| fileDescrComplete =
|
||||
let fInvDescr = FileDescr {fileDescrText = "", fileDescrPartNo = 0, fileDescrComplete = False}
|
||||
fInv = xftpFileInvitation fileName fileSize fInvDescr
|
||||
in Just (fInv, fileDescrText)
|
||||
in Just (fInv, fileDescrText, fileExpires)
|
||||
| otherwise = Nothing
|
||||
processContentItem :: Maybe GroupMember -> ChatItem 'CTGroup d -> MsgContent -> Maybe (FileInvitation, RcvFileDescrText) -> CM [(GrpMsgForward, VerifiedMsg 'Json)]
|
||||
processContentItem :: Maybe GroupMember -> ChatItem 'CTGroup d -> MsgContent -> Maybe (FileInvitation, RcvFileDescrText, Maybe UTCTime) -> CM [(GrpMsgForward, VerifiedMsg 'Json)]
|
||||
processContentItem member_ ChatItem {formattedText, meta, quotedItem, mentions} mc fInvDescr_ =
|
||||
if isNothing fInvDescr_ && not (msgContentHasText mc)
|
||||
then pure []
|
||||
else do
|
||||
let CIMeta {itemTs, itemSharedMsgId, itemTimed, showGroupAsSender} = meta
|
||||
quotedItemId_ = quoteItemId =<< quotedItem
|
||||
fInv_ = fst <$> fInvDescr_
|
||||
fInv_ = (\(fInv, _, _) -> fInv) <$> fInvDescr_
|
||||
(mc', _, mentions') = updatedMentionNames mc formattedText mentions
|
||||
mentions'' = M.map (\CIMention {memberId} -> MsgMention {memberId}) mentions'
|
||||
-- for channel messages default chat version range to membership range
|
||||
@@ -1472,11 +1472,11 @@ sendHistory user gInfo@GroupInfo {membership} m@GroupMember {activeConn = Just c
|
||||
-- TODO [knocking] send history to other scopes too?
|
||||
(chatMsgEvent, _) <- withStore $ \db -> prepareGroupMsg db user gInfo Nothing showGroupAsSender mc' mentions'' quotedItemId_ Nothing fInv_ itemTimed False
|
||||
pure $ VMUnsigned ChatMessage {chatVRange = senderVRange, msgId = itemSharedMsgId, chatMsgEvent}
|
||||
fileDescrEvents <- case (snd <$> fInvDescr_, itemSharedMsgId) of
|
||||
(Just fileDescrText, Just msgId) -> do
|
||||
fileDescrEvents <- case (fInvDescr_, itemSharedMsgId) of
|
||||
(Just (_, fileDescrText, fileExpires), Just msgId) -> do
|
||||
partSize <- asks $ xftpDescrPartSize . config
|
||||
let parts = splitFileDescr partSize fileDescrText
|
||||
pure . L.toList $ L.map (XMsgFileDescr msgId) parts
|
||||
pure . L.toList $ L.map (\fd -> XMsgFileDescr msgId fd fileExpires) parts
|
||||
_ -> pure []
|
||||
let fileDescrVMs = map (VMUnsigned . ChatMessage senderVRange Nothing) fileDescrEvents
|
||||
pure $ map ((,) fwd) (contentVM : fileDescrVMs)
|
||||
|
||||
@@ -41,6 +41,7 @@ import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (decodeLatin1)
|
||||
import Data.Time.Clock (NominalDiffTime, UTCTime, addUTCTime, diffUTCTime, getCurrentTime)
|
||||
import Data.Time.Clock.System (SystemTime (..), systemToUTCTime)
|
||||
import Data.Time.Format (defaultTimeLocale, formatTime)
|
||||
import qualified Data.UUID as UUID
|
||||
import qualified Data.UUID.V4 as V4
|
||||
@@ -75,7 +76,7 @@ import Simplex.Chat.Types.Preferences
|
||||
import Simplex.Chat.Types.Shared
|
||||
import Simplex.FileTransfer.Description (ValidFileDescription)
|
||||
import qualified Simplex.FileTransfer.Description as FD
|
||||
import Simplex.FileTransfer.Protocol (FilePartyI)
|
||||
import Simplex.FileTransfer.Protocol (FilePartyI, GrantedStorageTime (..))
|
||||
import qualified Simplex.FileTransfer.Transport as XFTP
|
||||
import Simplex.FileTransfer.Types (FileErrorType (..), RcvFileId, SndFileId)
|
||||
import Simplex.Messaging.Agent
|
||||
@@ -205,8 +206,9 @@ processAgentMsgSndFile _corrId aFileId msg = do
|
||||
liftIO $ updateCIFileStatus db user fileId status
|
||||
lookupChatItemByFileId db cxt user fileId
|
||||
toView $ CEvtSndFileProgressXFTP user ci ft sndProgress sndTotal
|
||||
SFDONE sndDescr rfds -> do
|
||||
withStore' $ \db -> setSndFTPrivateSndDescr db user fileId (fileDescrText sndDescr)
|
||||
SFDONE sndDescr rfds granted -> do
|
||||
let fileExpires = (\GSTExpires {epochSeconds} -> systemToUTCTime $ MkSystemTime epochSeconds 0) <$> granted
|
||||
withStore' $ \db -> setSndFTPrivateSndDescr db user fileId (fileDescrText sndDescr) fileExpires
|
||||
ci <- withStore $ \db -> lookupChatItemByFileId db cxt user fileId
|
||||
case ci of
|
||||
Nothing -> do
|
||||
@@ -232,7 +234,7 @@ processAgentMsgSndFile _corrId aFileId msg = do
|
||||
(rfd : extraRFDs, sft : _, SMDSnd, DirectChat ct) -> do
|
||||
withStore' $ \db -> createExtraSndFTDescrs db user fileId (map fileDescrText extraRFDs)
|
||||
conn@Connection {connId} <- liftEither $ contactSendConn_ ct
|
||||
sendFileDescriptions (ConnectionId connId) ((conn, sft, fileDescrText rfd) :| []) sharedMsgId >>= \case
|
||||
sendFileDescriptions (ConnectionId connId) ((conn, sft, fileDescrText rfd) :| []) sharedMsgId fileExpires >>= \case
|
||||
Just rs -> case L.last rs of
|
||||
Right ([msgDeliveryId], _) ->
|
||||
withStore' $ \db -> updateSndFTDeliveryXFTP db sft msgDeliveryId
|
||||
@@ -247,7 +249,7 @@ processAgentMsgSndFile _corrId aFileId msg = do
|
||||
extraRFDs = drop (length rfdsMemberFTs) rfds
|
||||
withStore' $ \db -> createExtraSndFTDescrs db user fileId (map fileDescrText extraRFDs)
|
||||
forM_ (L.nonEmpty rfdsMemberFTs) $ \rfdsMemberFTs' ->
|
||||
sendFileDescriptions (GroupId groupId) rfdsMemberFTs' sharedMsgId
|
||||
sendFileDescriptions (GroupId groupId) rfdsMemberFTs' sharedMsgId fileExpires
|
||||
ci' <- withStore $ \db -> do
|
||||
liftIO $ updateCIFileStatus db user fileId CIFSSndComplete
|
||||
getChatItemByFileId db cxt user fileId
|
||||
@@ -276,8 +278,8 @@ processAgentMsgSndFile _corrId aFileId msg = do
|
||||
where
|
||||
fileDescrText :: FilePartyI p => ValidFileDescription p -> T.Text
|
||||
fileDescrText = safeDecodeUtf8 . strEncode
|
||||
sendFileDescriptions :: ConnOrGroupId -> NonEmpty (Connection, SndFileTransfer, RcvFileDescrText) -> SharedMsgId -> CM (Maybe (NonEmpty (Either ChatError ([Int64], PQEncryption))))
|
||||
sendFileDescriptions connOrGroupId connsTransfersDescrs sharedMsgId = do
|
||||
sendFileDescriptions :: ConnOrGroupId -> NonEmpty (Connection, SndFileTransfer, RcvFileDescrText) -> SharedMsgId -> Maybe UTCTime -> CM (Maybe (NonEmpty (Either ChatError ([Int64], PQEncryption))))
|
||||
sendFileDescriptions connOrGroupId connsTransfersDescrs sharedMsgId fileExpires = do
|
||||
lift . void . withStoreBatch' $ \db -> L.map (\(_, sft, rfdText) -> updateSndFTDescrXFTP db user sft rfdText) connsTransfersDescrs
|
||||
partSize <- asks $ xftpDescrPartSize . config
|
||||
let connsIdsEvts = connDescrEvents partSize
|
||||
@@ -293,7 +295,7 @@ processAgentMsgSndFile _corrId aFileId msg = do
|
||||
where
|
||||
splitText :: (Connection, SndFileTransfer, RcvFileDescrText) -> [(Connection, (ConnOrGroupId, Maybe MsgSigning, ChatMsgEvent 'Json))]
|
||||
splitText (conn, _, rfdText) =
|
||||
map (\fileDescr -> (conn, (connOrGroupId, Nothing, XMsgFileDescr {msgId = sharedMsgId, fileDescr}))) (L.toList $ splitFileDescr partSize rfdText)
|
||||
map (\fileDescr -> (conn, (connOrGroupId, Nothing, XMsgFileDescr {msgId = sharedMsgId, fileDescr, fileExpires}))) (L.toList $ splitFileDescr partSize rfdText)
|
||||
toMsgReq :: (Connection, (ConnOrGroupId, Maybe MsgSigning, ChatMsgEvent 'Json)) -> SndMessage -> ChatMsgReq
|
||||
toMsgReq (conn, _) SndMessage {msgId, msgBody} =
|
||||
(conn, MsgFlags {notification = hasNotification XMsgFileDescr_}, (vrValue msgBody, [msgId]))
|
||||
@@ -548,7 +550,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
let ct'' = ct' {activeConn = Just conn''} :: Contact
|
||||
case event of
|
||||
XMsgNew mc -> newContentMessage ct'' mc msg msgMeta
|
||||
XMsgFileDescr sharedMsgId fileDescr -> messageFileDescription ct'' sharedMsgId fileDescr
|
||||
XMsgFileDescr sharedMsgId fileDescr fileExpires -> messageFileDescription ct'' sharedMsgId fileDescr fileExpires
|
||||
XMsgUpdate sharedMsgId mContent _ ttl live _msgScope _ -> messageUpdate ct'' sharedMsgId mContent msg msgMeta ttl live
|
||||
XMsgDel sharedMsgId _ _ _ -> messageDelete ct'' sharedMsgId msg msgMeta
|
||||
XMsgReact sharedMsgId _ _ reaction add -> directMsgReaction ct'' sharedMsgId reaction add msg msgMeta
|
||||
@@ -1033,7 +1035,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
where
|
||||
MsgContainer {scope, asGroup} = mc
|
||||
-- file description is always allowed, to allow sending files to support scope
|
||||
XMsgFileDescr sharedMsgId fileDescr -> groupMessageFileDescription gInfo' (Just m'') sharedMsgId fileDescr
|
||||
XMsgFileDescr sharedMsgId fileDescr fileExpires -> groupMessageFileDescription gInfo' (Just m'') sharedMsgId fileDescr fileExpires
|
||||
XMsgUpdate sharedMsgId mContent mentions ttl live msgScope asGroup_ ->
|
||||
checkSendAsGroup asGroup_ $
|
||||
memberCanSend (Just m'') msgScope $
|
||||
@@ -1905,16 +1907,16 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
ChatConfig {autoAcceptFileSize = sz} <- asks config
|
||||
when (sz > fileSize) $ receiveFileEvt' user ft False Nothing Nothing >>= toView
|
||||
|
||||
messageFileDescription :: Contact -> SharedMsgId -> FileDescr -> CM ()
|
||||
messageFileDescription Contact {contactId} sharedMsgId fileDescr = do
|
||||
messageFileDescription :: Contact -> SharedMsgId -> FileDescr -> Maybe UTCTime -> CM ()
|
||||
messageFileDescription Contact {contactId} sharedMsgId fileDescr fileExpires = do
|
||||
(fileId, aci) <- withStore $ \db -> do
|
||||
fileId <- getFileIdBySharedMsgId db userId contactId sharedMsgId
|
||||
aci <- getChatItemByFileId db cxt user fileId
|
||||
pure (fileId, aci)
|
||||
processFDMessage fileId aci fileDescr
|
||||
processFDMessage fileId aci fileDescr fileExpires
|
||||
|
||||
groupMessageFileDescription :: GroupInfo -> Maybe GroupMember -> SharedMsgId -> FileDescr -> CM (Maybe DeliveryTaskContext)
|
||||
groupMessageFileDescription g@GroupInfo {groupId} m_ sharedMsgId fileDescr = do
|
||||
groupMessageFileDescription :: GroupInfo -> Maybe GroupMember -> SharedMsgId -> FileDescr -> Maybe UTCTime -> CM (Maybe DeliveryTaskContext)
|
||||
groupMessageFileDescription g@GroupInfo {groupId} m_ sharedMsgId fileDescr fileExpires = do
|
||||
(fileId, aci) <- withStore $ \db -> do
|
||||
fileId <- getGroupFileIdBySharedMsgId db userId groupId sharedMsgId
|
||||
aci <- getChatItemByFileId db cxt user fileId
|
||||
@@ -1925,17 +1927,18 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
-- in processFDMessage some paths are programmed as errors,
|
||||
-- for example failure on not approved relays (CEFileNotApproved).
|
||||
-- we catch error, so that even if processFDMessage fails, message can still be forwarded.
|
||||
processFDMessage fileId aci fileDescr `catchAllErrors` \_ -> pure ()
|
||||
processFDMessage fileId aci fileDescr fileExpires `catchAllErrors` \_ -> pure ()
|
||||
pure $ Just $ infoToDeliveryContext g scopeInfo (isChannelDir chatDir)
|
||||
| otherwise -> messageError "x.msg.file.descr: file/sender mismatch" $> Nothing
|
||||
_ -> messageError "x.msg.file.descr: invalid file description part" $> Nothing
|
||||
|
||||
processFDMessage :: FileTransferId -> AChatItem -> FileDescr -> CM ()
|
||||
processFDMessage fileId aci fileDescr = do
|
||||
processFDMessage :: FileTransferId -> AChatItem -> FileDescr -> Maybe UTCTime -> CM ()
|
||||
processFDMessage fileId aci fileDescr fileExpires = do
|
||||
ft <- withStore $ \db -> getRcvFileTransfer db user fileId
|
||||
unless (rcvFileCompleteOrCancelled ft) $ do
|
||||
(rfd@RcvFileDescr {fileDescrComplete}, ft'@RcvFileTransfer {fileStatus, xftpRcvFile, cryptoArgs, fileInvitation = FileInvitation {fileSize}}) <- withStore $ \db -> do
|
||||
rfd <- appendRcvFD db userId fileId fileDescr
|
||||
forM_ fileExpires $ liftIO . setFileExpiration db user fileId
|
||||
-- reading second time in the same transaction as appending description
|
||||
-- to prevent race condition with accept
|
||||
ft' <- getRcvFileTransfer db user fileId
|
||||
@@ -1962,7 +1965,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
_ -> pure (Nothing, CIFSRcvInvitation, ft)
|
||||
let RcvFileTransfer {cryptoArgs} = ft'
|
||||
fileSource = (`CryptoFile` cryptoArgs) <$> filePath
|
||||
pure (ft', CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol})
|
||||
pure (ft', CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol, fileExpires = Nothing})
|
||||
|
||||
mkValidFileInvitation :: FileInvitation -> FileInvitation
|
||||
mkValidFileInvitation fInv@FileInvitation {fileName} = fInv {fileName = safeFileNameStr fileName}
|
||||
@@ -2418,7 +2421,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
inline <- receiveInlineMode fInv' Nothing fileChunkSize
|
||||
RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFileTransfer db userId ct fInv' inline fileChunkSize
|
||||
let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP
|
||||
ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol}
|
||||
ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol, fileExpires = Nothing}
|
||||
content = ciContentNoParse $ CIRcvMsgContent $ MCFile ""
|
||||
(ci, cInfo) <- saveRcvChatItem' user (CDDirectRcv ct) msg sharedMsgId_ brokerTs content ciFile Nothing False M.empty
|
||||
toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv cInfo ci]
|
||||
@@ -2433,7 +2436,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
inline <- receiveInlineMode fInv' Nothing fileChunkSize
|
||||
RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvGroupFileTransfer db userId gInfo (Just m) FTNormal sharedMsgId_ fInv' inline fileChunkSize
|
||||
let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP
|
||||
ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol}
|
||||
ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol, fileExpires = Nothing}
|
||||
content = ciContentNoParse $ CIRcvMsgContent $ MCFile ""
|
||||
(ci, cInfo) <- saveRcvChatItem' user (CDGroupRcv gInfo Nothing m) msg sharedMsgId_ brokerTs content ciFile Nothing False M.empty
|
||||
ci' <- blockedMemberCI gInfo m ci
|
||||
@@ -3875,7 +3878,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
where
|
||||
MsgContainer {scope} = mc
|
||||
-- file description is always allowed, to allow sending files to support scope
|
||||
XMsgFileDescr sharedMsgId fileDescr -> void $ groupMessageFileDescription gInfo author_ sharedMsgId fileDescr
|
||||
XMsgFileDescr sharedMsgId fileDescr fileExpires -> void $ groupMessageFileDescription gInfo author_ sharedMsgId fileDescr fileExpires
|
||||
XMsgUpdate sharedMsgId mContent mentions ttl live msgScope asGroup_ ->
|
||||
void $ memberCanSend author_ msgScope $ groupMessageUpdate gInfo author_ sharedMsgId mContent mentions msgScope rcvMsg msgTs ttl live asGroup_
|
||||
XMsgDel sharedMsgId memId scope_ _ -> void $ groupMessageDelete gInfo author_ sharedMsgId memId scope_ False rcvMsg msgTs
|
||||
|
||||
@@ -687,7 +687,8 @@ data CIFile (d :: MsgDirection) = CIFile
|
||||
fileSize :: Integer,
|
||||
fileSource :: Maybe CryptoFile, -- local file path with optional key and nonce
|
||||
fileStatus :: CIFileStatus d,
|
||||
fileProtocol :: FileProtocol
|
||||
fileProtocol :: FileProtocol,
|
||||
fileExpires :: Maybe UTCTime
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
|
||||
@@ -446,7 +446,7 @@ signChatMsgBody MsgSigning {bindingTag, bindingData, keyRef, privKey} msgBody =
|
||||
|
||||
data ChatMsgEvent (e :: MsgEncoding) where
|
||||
XMsgNew :: MsgContainer -> ChatMsgEvent 'Json
|
||||
XMsgFileDescr :: {msgId :: SharedMsgId, fileDescr :: FileDescr} -> ChatMsgEvent 'Json
|
||||
XMsgFileDescr :: {msgId :: SharedMsgId, fileDescr :: FileDescr, fileExpires :: Maybe UTCTime} -> ChatMsgEvent 'Json
|
||||
XMsgUpdate :: {msgId :: SharedMsgId, content :: MsgContent, mentions :: Map MemberName MsgMention, ttl :: Maybe Int, live :: Maybe Bool, scope :: Maybe MsgScope, asGroup :: Maybe Bool} -> ChatMsgEvent 'Json
|
||||
XMsgDel :: {msgId :: SharedMsgId, memberId :: Maybe MemberId, scope :: Maybe MsgScope, onlyHistory :: Bool} -> ChatMsgEvent 'Json
|
||||
XMsgDeleted :: ChatMsgEvent 'Json
|
||||
@@ -517,7 +517,7 @@ isForwardedGroupMsg ev = case ev of
|
||||
XMsgNew mc -> case mc of
|
||||
MsgContainer {file = Just FileInvitation {fileInline = Just _}} -> False
|
||||
_ -> True
|
||||
XMsgFileDescr _ _ -> True
|
||||
XMsgFileDescr {} -> True
|
||||
XMsgUpdate {} -> True
|
||||
XMsgDel {} -> True
|
||||
XMsgReact {} -> True
|
||||
@@ -1239,7 +1239,7 @@ instance StrEncoding ACMEventTag where
|
||||
toCMEventTag :: ChatMsgEvent e -> CMEventTag e
|
||||
toCMEventTag msg = case msg of
|
||||
XMsgNew _ -> XMsgNew_
|
||||
XMsgFileDescr _ _ -> XMsgFileDescr_
|
||||
XMsgFileDescr {} -> XMsgFileDescr_
|
||||
XMsgUpdate {} -> XMsgUpdate_
|
||||
XMsgDel {} -> XMsgDel_
|
||||
XMsgDeleted -> XMsgDeleted_
|
||||
@@ -1391,7 +1391,7 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do
|
||||
msg :: CMEventTag 'Json -> Either String (ChatMsgEvent 'Json)
|
||||
msg = \case
|
||||
XMsgNew_ -> XMsgNew <$> JT.parseEither parseJSON (J.Object params)
|
||||
XMsgFileDescr_ -> XMsgFileDescr <$> p "msgId" <*> p "fileDescr"
|
||||
XMsgFileDescr_ -> XMsgFileDescr <$> p "msgId" <*> p "fileDescr" <*> opt "fileExpires"
|
||||
XMsgUpdate_ -> do
|
||||
msgId' <- p "msgId"
|
||||
content <- p "content"
|
||||
@@ -1482,7 +1482,7 @@ chatToAppMessage chatMsg@ChatMessage {chatVRange, msgId, chatMsgEvent} = case en
|
||||
XMsgNew mc -> case toJSON mc of
|
||||
J.Object obj -> obj
|
||||
_ -> JM.empty
|
||||
XMsgFileDescr msgId' fileDescr -> o ["msgId" .= msgId', "fileDescr" .= fileDescr]
|
||||
XMsgFileDescr msgId' fileDescr fileExpires -> o $ ("fileExpires" .=? fileExpires) ["msgId" .= msgId', "fileDescr" .= fileDescr]
|
||||
XMsgUpdate {msgId = msgId', content, mentions, ttl, live, scope, asGroup} -> o $ ("asGroup" .=? asGroup) $ ("ttl" .=? ttl) $ ("live" .=? live) $ ("scope" .=? scope) $ ("mentions" .=? nonEmptyMap mentions) ["msgId" .= msgId', "content" .= content]
|
||||
XMsgDel msgId' memberId scope onlyHistory -> o $ ("memberId" .=? memberId) $ ("scope" .=? scope) $ ("onlyHistory" .=? justTrue onlyHistory) ["msgId" .= msgId']
|
||||
XMsgDeleted -> JM.empty
|
||||
|
||||
@@ -20,6 +20,7 @@ module Simplex.Chat.Store.Files
|
||||
createSndFileTransferXFTP,
|
||||
createSndFTDescrXFTP,
|
||||
setSndFTPrivateSndDescr,
|
||||
setFileExpiration,
|
||||
getSndFTPrivateSndDescr,
|
||||
updateSndFTDescrXFTP,
|
||||
createExtraSndFTDescrs,
|
||||
@@ -203,13 +204,21 @@ createSndFTDescrXFTP db User {userId} m Connection {connId} FileTransferMeta {fi
|
||||
"INSERT INTO snd_files (file_id, file_status, file_descr_id, group_member_id, connection_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?)"
|
||||
(fileId, fileStatus, fileDescrId, groupMemberId' <$> m, connId, currentTs, currentTs)
|
||||
|
||||
setSndFTPrivateSndDescr :: DB.Connection -> User -> FileTransferId -> Text -> IO ()
|
||||
setSndFTPrivateSndDescr db User {userId} fileId sfdText = do
|
||||
setSndFTPrivateSndDescr :: DB.Connection -> User -> FileTransferId -> Text -> Maybe UTCTime -> IO ()
|
||||
setSndFTPrivateSndDescr db User {userId} fileId sfdText fileExpires = do
|
||||
currentTs <- getCurrentTime
|
||||
DB.execute
|
||||
db
|
||||
"UPDATE files SET private_snd_file_descr = ?, updated_at = ? WHERE user_id = ? AND file_id = ?"
|
||||
(sfdText, currentTs, userId, fileId)
|
||||
"UPDATE files SET private_snd_file_descr = ?, file_expires_at = ?, updated_at = ? WHERE user_id = ? AND file_id = ?"
|
||||
(sfdText, fileExpires, currentTs, userId, fileId)
|
||||
|
||||
setFileExpiration :: DB.Connection -> User -> FileTransferId -> UTCTime -> IO ()
|
||||
setFileExpiration db User {userId} fileId fileExpires = do
|
||||
currentTs <- getCurrentTime
|
||||
DB.execute
|
||||
db
|
||||
"UPDATE files SET file_expires_at = ?, updated_at = ? WHERE user_id = ? AND file_id = ?"
|
||||
(fileExpires, currentTs, userId, fileId)
|
||||
|
||||
getSndFTPrivateSndDescr :: DB.Connection -> User -> FileTransferId -> IO (Maybe Text)
|
||||
getSndFTPrivateSndDescr db User {userId} fileId =
|
||||
|
||||
@@ -1093,7 +1093,7 @@ getLocalChatPreview_ db user (LocalChatPD _ noteFolderId lastItemId_ stats) = do
|
||||
|
||||
-- this function can be changed so it never fails, not only avoid failure on invalid json
|
||||
toLocalChatItem :: UTCTime -> ChatItemRow -> Either StoreError (CChatItem 'CTLocal)
|
||||
toLocalChatItem currentTs ((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sentViaProxy, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive, BI userMention, BI hasLink, msgSigned) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) =
|
||||
toLocalChatItem currentTs ((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sentViaProxy, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive, BI userMention, BI hasLink, msgSigned) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_, fileExpires)) =
|
||||
chatItem $ fromRight invalid $ dbParseACIContent itemContentText
|
||||
where
|
||||
invalid = ACIContent msgDir $ CIInvalidJSON itemContentText
|
||||
@@ -1113,7 +1113,7 @@ toLocalChatItem currentTs ((itemId, itemTs, AMsgDirection msgDir, itemContentTex
|
||||
(Just fileId, Just fileName, Just fileSize, Just fileProtocol) ->
|
||||
let cfArgs = CFArgs <$> fileKey <*> fileNonce
|
||||
fileSource = (`CryptoFile` cfArgs) <$> filePath
|
||||
in Just CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol}
|
||||
in Just CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol, fileExpires}
|
||||
_ -> Nothing
|
||||
cItem :: MsgDirectionI d => SMsgDirection d -> CIDirection 'CTLocal d -> CIStatus d -> CIContent d -> Maybe (CIFile d) -> CChatItem 'CTLocal
|
||||
cItem d chatDir ciStatus content file =
|
||||
@@ -2276,7 +2276,7 @@ updateLocalChatItemsRead db User {userId} noteFolderId = do
|
||||
|]
|
||||
(CISRcvRead, currentTs, userId, noteFolderId, CISRcvNew)
|
||||
|
||||
type MaybeCIFIleRow = (Maybe Int64, Maybe String, Maybe Integer, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe ACIFileStatus, Maybe FileProtocol)
|
||||
type MaybeCIFIleRow = (Maybe Int64, Maybe String, Maybe Integer, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe ACIFileStatus, Maybe FileProtocol, Maybe UTCTime)
|
||||
|
||||
type ChatItemModeRow = (Maybe Int, Maybe UTCTime, Maybe BoolInt, BoolInt, BoolInt, Maybe MsgVerified)
|
||||
|
||||
@@ -2304,7 +2304,7 @@ toQuote (quotedItemId, quotedSharedMsgId, quotedSentAt, quotedMsgContent, _) dir
|
||||
|
||||
-- this function can be changed so it never fails, not only avoid failure on invalid json
|
||||
toDirectChatItem :: UTCTime -> ChatItemRow :. QuoteRow -> Either StoreError (CChatItem 'CTDirect)
|
||||
toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sentViaProxy, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive, BI userMention, BI hasLink, msgSigned) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. quoteRow) =
|
||||
toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sentViaProxy, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive, BI userMention, BI hasLink, msgSigned) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_, fileExpires)) :. quoteRow) =
|
||||
chatItem $ fromRight invalid $ dbParseACIContent itemContentText
|
||||
where
|
||||
invalid = ACIContent msgDir $ CIInvalidJSON itemContentText
|
||||
@@ -2324,7 +2324,7 @@ toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentT
|
||||
(Just fileId, Just fileName, Just fileSize, Just fileProtocol) ->
|
||||
let cfArgs = CFArgs <$> fileKey <*> fileNonce
|
||||
fileSource = (`CryptoFile` cfArgs) <$> filePath
|
||||
in Just CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol}
|
||||
in Just CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol, fileExpires}
|
||||
_ -> Nothing
|
||||
cItem :: MsgDirectionI d => SMsgDirection d -> CIDirection 'CTDirect d -> CIStatus d -> CIContent d -> Maybe (CIFile d) -> CChatItem 'CTDirect
|
||||
cItem d chatDir ciStatus content file =
|
||||
@@ -2379,7 +2379,7 @@ toGroupChatItem
|
||||
:. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt)
|
||||
:. forwardedFromRow
|
||||
:. (timedTTL, timedDeleteAt, itemLive, BI userMention, BI hasLink, msgSigned)
|
||||
:. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)
|
||||
:. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_, fileExpires)
|
||||
)
|
||||
:. (forwardedByMember, BI showGroupAsSender)
|
||||
:. memberRow_
|
||||
@@ -2414,7 +2414,7 @@ toGroupChatItem
|
||||
(Just fileId, Just fileName, Just fileSize, Just fileProtocol) ->
|
||||
let cfArgs = CFArgs <$> fileKey <*> fileNonce
|
||||
fileSource = (`CryptoFile` cfArgs) <$> filePath
|
||||
in Just CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol}
|
||||
in Just CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol, fileExpires}
|
||||
_ -> Nothing
|
||||
cItem :: MsgDirectionI d => SMsgDirection d -> CIDirection 'CTGroup d -> CIStatus d -> CIContent d -> Maybe (CIFile d) -> CChatItem 'CTGroup
|
||||
cItem d chatDir ciStatus content file =
|
||||
@@ -2705,7 +2705,7 @@ getDirectChatItem db User {userId} contactId itemId = ExceptT $ do
|
||||
i.fwd_from_group_type, i.fwd_from_group_link, i.fwd_from_public_group_id, i.fwd_from_member_id, i.fwd_from_shared_msg_id,
|
||||
i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, i.has_link, i.msg_signed,
|
||||
-- CIFile
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol,
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, f.file_expires_at,
|
||||
-- DirectQuote
|
||||
ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent
|
||||
FROM chat_items i
|
||||
@@ -3099,7 +3099,7 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do
|
||||
i.fwd_from_group_type, i.fwd_from_group_link, i.fwd_from_public_group_id, i.fwd_from_member_id, i.fwd_from_shared_msg_id,
|
||||
i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, i.has_link, i.msg_signed,
|
||||
-- CIFile
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol,
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, f.file_expires_at,
|
||||
-- CIMeta forwardedByMember, showGroupAsSender
|
||||
i.forwarded_by_group_member_id, i.show_group_as_sender,
|
||||
-- GroupMember
|
||||
@@ -3212,7 +3212,7 @@ getLocalChatItem db User {userId} folderId itemId = ExceptT $ do
|
||||
i.fwd_from_group_type, i.fwd_from_group_link, i.fwd_from_public_group_id, i.fwd_from_member_id, i.fwd_from_shared_msg_id,
|
||||
i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, i.has_link, i.msg_signed,
|
||||
-- CIFile
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, f.file_expires_at
|
||||
FROM chat_items i
|
||||
LEFT JOIN files f ON f.chat_item_id = i.chat_item_id
|
||||
WHERE i.user_id = ? AND i.note_folder_id = ? AND i.chat_item_id = ?
|
||||
|
||||
@@ -48,6 +48,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20260720_server_roles
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260723_contact_request_rejection
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260813_auto_accept_group_invitations
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260822_forward_link
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260828_file_expiry
|
||||
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
|
||||
|
||||
schemaMigrations :: [(String, Text, Maybe Text)]
|
||||
@@ -95,7 +96,8 @@ schemaMigrations =
|
||||
("20260720_server_roles", m20260720_server_roles, Just down_m20260720_server_roles),
|
||||
("20260723_contact_request_rejection", m20260723_contact_request_rejection, Just down_m20260723_contact_request_rejection),
|
||||
("20260813_auto_accept_group_invitations", m20260813_auto_accept_group_invitations, Just down_m20260813_auto_accept_group_invitations),
|
||||
("20260822_forward_link", m20260822_forward_link, Just down_m20260822_forward_link)
|
||||
("20260822_forward_link", m20260822_forward_link, Just down_m20260822_forward_link),
|
||||
("20260828_file_expiry", m20260828_file_expiry, Just down_m20260828_file_expiry)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Store.Postgres.Migrations.M20260828_file_expiry where
|
||||
|
||||
import Data.Text (Text)
|
||||
import Text.RawString.QQ (r)
|
||||
|
||||
m20260828_file_expiry :: Text
|
||||
m20260828_file_expiry =
|
||||
[r|
|
||||
ALTER TABLE files ADD COLUMN file_expires_at TIMESTAMPTZ;
|
||||
|]
|
||||
|
||||
down_m20260828_file_expiry :: Text
|
||||
down_m20260828_file_expiry =
|
||||
[r|
|
||||
ALTER TABLE files DROP COLUMN file_expires_at;
|
||||
|]
|
||||
@@ -770,7 +770,8 @@ CREATE TABLE test_chat_schema.files (
|
||||
shared_msg_id bytea,
|
||||
file_type text DEFAULT 'normal'::text NOT NULL,
|
||||
roster_transfer_id bigint,
|
||||
file_digest bytea
|
||||
file_digest bytea,
|
||||
file_expires_at timestamp with time zone
|
||||
);
|
||||
|
||||
|
||||
|
||||
@@ -171,6 +171,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20260720_server_roles
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260723_contact_request_rejection
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260813_auto_accept_group_invitations
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260822_forward_link
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260828_file_expiry
|
||||
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
|
||||
|
||||
schemaMigrations :: [(String, Query, Maybe Query)]
|
||||
@@ -341,7 +342,8 @@ schemaMigrations =
|
||||
("20260720_server_roles", m20260720_server_roles, Just down_m20260720_server_roles),
|
||||
("20260723_contact_request_rejection", m20260723_contact_request_rejection, Just down_m20260723_contact_request_rejection),
|
||||
("20260813_auto_accept_group_invitations", m20260813_auto_accept_group_invitations, Just down_m20260813_auto_accept_group_invitations),
|
||||
("20260822_forward_link", m20260822_forward_link, Just down_m20260822_forward_link)
|
||||
("20260822_forward_link", m20260822_forward_link, Just down_m20260822_forward_link),
|
||||
("20260828_file_expiry", m20260828_file_expiry, Just down_m20260828_file_expiry)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Store.SQLite.Migrations.M20260828_file_expiry where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20260828_file_expiry :: Query
|
||||
m20260828_file_expiry =
|
||||
[sql|
|
||||
ALTER TABLE files ADD COLUMN file_expires_at TEXT;
|
||||
|]
|
||||
|
||||
down_m20260828_file_expiry :: Query
|
||||
down_m20260828_file_expiry =
|
||||
[sql|
|
||||
ALTER TABLE files DROP COLUMN file_expires_at;
|
||||
|]
|
||||
@@ -2,7 +2,7 @@ Query:
|
||||
SELECT
|
||||
f.snd_file_id, f.snd_file_entity_id, f.user_id, f.num_recipients, f.prefix_path,
|
||||
c.snd_file_chunk_id, c.chunk_no, c.chunk_offset, c.chunk_size, c.digest,
|
||||
r.snd_file_chunk_replica_id, r.replica_id, r.replica_key, r.replica_status, r.delay, r.retries
|
||||
r.snd_file_chunk_replica_id, r.replica_id, r.replica_key, r.replica_status, r.delay, r.retries, r.replica_expires_at
|
||||
FROM snd_file_chunk_replicas r
|
||||
JOIN xftp_servers s ON s.xftp_server_id = r.xftp_server_id
|
||||
JOIN snd_file_chunks c ON c.snd_file_chunk_id = r.snd_file_chunk_id
|
||||
@@ -31,7 +31,7 @@ SEARCH sb USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN
|
||||
|
||||
Query:
|
||||
SELECT
|
||||
r.snd_file_chunk_replica_id, r.replica_id, r.replica_key, r.replica_status, r.delay, r.retries,
|
||||
r.snd_file_chunk_replica_id, r.replica_id, r.replica_key, r.replica_status, r.delay, r.retries, r.replica_expires_at,
|
||||
s.xftp_host, s.xftp_port, s.xftp_key_hash
|
||||
FROM snd_file_chunk_replicas r
|
||||
JOIN xftp_servers s ON s.xftp_server_id = r.xftp_server_id
|
||||
@@ -96,7 +96,7 @@ Plan:
|
||||
SEARCH snd_file_chunks USING INDEX idx_snd_file_chunks_snd_file_id (snd_file_id=?)
|
||||
|
||||
Query:
|
||||
SELECT snd_file_entity_id, user_id, path, src_file_key, src_file_nonce, num_recipients, digest, prefix_path, key, nonce, status, deleted, redirect_size, redirect_digest
|
||||
SELECT snd_file_entity_id, user_id, path, src_file_key, src_file_nonce, num_recipients, digest, prefix_path, key, nonce, status, deleted, redirect_size, redirect_digest, storage_time
|
||||
FROM snd_files
|
||||
WHERE snd_file_id = ?
|
||||
|
||||
@@ -686,8 +686,8 @@ Plan:
|
||||
|
||||
Query:
|
||||
INSERT INTO snd_file_chunk_replicas
|
||||
(snd_file_chunk_id, replica_number, xftp_server_id, replica_id, replica_key, replica_status)
|
||||
VALUES (?,?,?,?,?,?)
|
||||
(snd_file_chunk_id, replica_number, xftp_server_id, replica_id, replica_key, replica_status, replica_expires_at)
|
||||
VALUES (?,?,?,?,?,?,?)
|
||||
|
||||
Plan:
|
||||
|
||||
@@ -1182,7 +1182,7 @@ Plan:
|
||||
Query: INSERT INTO snd_file_chunks (snd_file_id, chunk_no, chunk_offset, chunk_size, digest) VALUES (?,?,?,?,?)
|
||||
Plan:
|
||||
|
||||
Query: INSERT INTO snd_files (snd_file_entity_id, user_id, path, src_file_key, src_file_nonce, num_recipients, prefix_path, key, nonce, status, redirect_size, redirect_digest) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
Query: INSERT INTO snd_files (snd_file_entity_id, user_id, path, src_file_key, src_file_nonce, num_recipients, prefix_path, key, nonce, status, redirect_size, redirect_digest, storage_time) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
Plan:
|
||||
|
||||
Query: INSERT INTO snd_message_bodies (agent_msg) VALUES (?) RETURNING snd_message_body_id
|
||||
|
||||
@@ -1357,7 +1357,7 @@ Query:
|
||||
i.fwd_from_group_type, i.fwd_from_group_link, i.fwd_from_public_group_id, i.fwd_from_member_id, i.fwd_from_shared_msg_id,
|
||||
i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, i.has_link, i.msg_signed,
|
||||
-- CIFile
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, f.file_expires_at
|
||||
FROM chat_items i
|
||||
LEFT JOIN files f ON f.chat_item_id = i.chat_item_id
|
||||
WHERE i.user_id = ? AND i.note_folder_id = ? AND i.chat_item_id = ?
|
||||
@@ -1375,7 +1375,7 @@ Query:
|
||||
i.fwd_from_group_type, i.fwd_from_group_link, i.fwd_from_public_group_id, i.fwd_from_member_id, i.fwd_from_shared_msg_id,
|
||||
i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, i.has_link, i.msg_signed,
|
||||
-- CIFile
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol,
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, f.file_expires_at,
|
||||
-- CIMeta forwardedByMember, showGroupAsSender
|
||||
i.forwarded_by_group_member_id, i.show_group_as_sender,
|
||||
-- GroupMember
|
||||
@@ -1432,7 +1432,7 @@ Query:
|
||||
i.fwd_from_group_type, i.fwd_from_group_link, i.fwd_from_public_group_id, i.fwd_from_member_id, i.fwd_from_shared_msg_id,
|
||||
i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, i.has_link, i.msg_signed,
|
||||
-- CIFile
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol,
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, f.file_expires_at,
|
||||
-- DirectQuote
|
||||
ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent
|
||||
FROM chat_items i
|
||||
@@ -7710,11 +7710,15 @@ Query: UPDATE files SET file_crypto_key = ?, file_crypto_nonce = ?, updated_at =
|
||||
Plan:
|
||||
SEARCH files USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query: UPDATE files SET file_expires_at = ?, updated_at = ? WHERE user_id = ? AND file_id = ?
|
||||
Plan:
|
||||
SEARCH files USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query: UPDATE files SET file_path = ?, ci_file_status = ?, updated_at = ? WHERE user_id = ? AND file_id = ?
|
||||
Plan:
|
||||
SEARCH files USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query: UPDATE files SET private_snd_file_descr = ?, updated_at = ? WHERE user_id = ? AND file_id = ?
|
||||
Query: UPDATE files SET private_snd_file_descr = ?, file_expires_at = ?, updated_at = ? WHERE user_id = ? AND file_id = ?
|
||||
Plan:
|
||||
SEARCH files USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
|
||||
@@ -301,7 +301,8 @@ CREATE TABLE files(
|
||||
shared_msg_id BLOB,
|
||||
file_type TEXT NOT NULL DEFAULT 'normal',
|
||||
roster_transfer_id INTEGER,
|
||||
file_digest BLOB
|
||||
file_digest BLOB,
|
||||
file_expires_at TEXT
|
||||
) STRICT;
|
||||
CREATE TABLE snd_files(
|
||||
file_id INTEGER NOT NULL REFERENCES files ON DELETE CASCADE,
|
||||
|
||||
+4
-1
@@ -635,7 +635,9 @@ xftpServerConfig =
|
||||
newFileBasicAuth = Nothing,
|
||||
controlPortUserAuth = Nothing,
|
||||
controlPortAdminAuth = Nothing,
|
||||
fileExpiration = Just defaultFileExpiration,
|
||||
fileExpiration = defaultFileExpiration,
|
||||
fileStorageEntitlements = mempty,
|
||||
entitlementKeys = mempty,
|
||||
fileTimeout = 10000000,
|
||||
inactiveClientExpiration = Just defaultInactiveClientExpiration,
|
||||
xftpCredentials =
|
||||
@@ -647,6 +649,7 @@ xftpServerConfig =
|
||||
httpCredentials = Nothing,
|
||||
webStaticPath = Nothing,
|
||||
xftpServerVRange = supportedFileServerVRange,
|
||||
information = Nothing,
|
||||
logStatsInterval = Nothing,
|
||||
logStatsStartTime = 0,
|
||||
serverStatsLogFile = "tests/tmp/xftp-server-stats.daily.log",
|
||||
|
||||
Reference in New Issue
Block a user