Compare commits

..

15 Commits

Author SHA1 Message Date
Evgeny Poberezkin ea6c5bfb0b 5.6.1: ios 206, android 193, desktop 36 2024-04-04 08:58:24 +01:00
Stanislav Dmitrenko 14c279d1e0 android: possibly, prevent TooManyRequests exception (#3989)
* android: possibly, prevent TooManyRequests exception

* new line
2024-04-03 15:15:44 +01:00
Stanislav Dmitrenko d2de81100d android: workaround of pager's bug (#3988) 2024-04-03 15:11:04 +01:00
Stanislav Dmitrenko 9b28ae6d9e desktop: remote connection host/port fix (#3987) 2024-04-03 11:58:32 +01:00
Evgeny Poberezkin ea862a8f34 core: 5.6.1.1 (simplexmq 5.6.2.1) 2024-04-03 11:43:42 +01:00
Evgeny Poberezkin 2bd1a82b7d core: revert "deps: switch to base64 via simplexmq (#3957)" (#3985)
* Revert "deps: switch to base64 via simplexmq (#3957)"

This reverts commit d65137882b.

* update simplexmq
2024-04-03 10:47:38 +01:00
Evgeny Poberezkin 97a37634ef ios: 5.6.1 build 205 2024-04-03 01:47:01 +01:00
Stanislav Dmitrenko 28fbc1cd84 desktop: correct height of a window (#3982) 2024-04-02 20:05:14 +01:00
Stanislav Dmitrenko b8ee2af5b7 deskop: show window icon (#3983)
* deskop: show window icon

* size
2024-04-02 18:57:23 +01:00
Stanislav Dmitrenko c234809894 ios: improvement of chat item context menu (#3981)
* ios: improvement of chat item context menu

* rename

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-04-02 17:00:24 +01:00
Evgeny Poberezkin e1a997877a rfc: groups based on super-peers (WIP) (#3917) 2024-04-01 15:04:13 +01:00
Evgeny Poberezkin 69218952c3 core: 5.6.1.0 2024-04-01 14:36:45 +01:00
Evgeny Poberezkin dc2591b0bf ios: build settings (#3978) 2024-04-01 13:55:26 +01:00
Evgeny Poberezkin 2272b77b53 ios: refactor notifications (#3976)
* ios: refactor notifications

* refactor
2024-04-01 13:52:28 +01:00
Evgeny Poberezkin d90e2f4436 core: remove mtl typeclasses to reduce overhead (#3975)
* core: remove mtl typeclasses to reduce overhead

* strict data, optimization

* update simplexmq, clean up

* un-unlift attachRevHTTP2Client

* remote

---------

Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com>
2024-04-01 13:34:45 +01:00
42 changed files with 1062 additions and 846 deletions
+1 -5
View File
@@ -22,11 +22,7 @@ struct SimpleXApp: App {
init() {
DispatchQueue.global(qos: .background).sync {
// we have to use debug profile file name without extension here because .hp extension is added by profiler
haskell_init(
getAppEventLogPath().path,
getAppDebugProfilePrefixPath().path
)
haskell_init()
// hs_init(0, nil)
}
UserDefaults.standard.register(defaults: appDefaults)
@@ -70,14 +70,14 @@ struct CIImageView: View {
}
private func imageView(_ img: UIImage) -> some View {
let w = img.size.width <= img.size.height ? maxWidth * 0.75 : img.imageData == nil ? .infinity : maxWidth
let w = img.size.width <= img.size.height ? maxWidth * 0.75 : maxWidth
DispatchQueue.main.async { imgWidth = w }
return ZStack(alignment: .topTrailing) {
if img.imageData == nil {
Image(uiImage: img)
.resizable()
.scaledToFit()
.frame(maxWidth: w)
.frame(width: w)
} else {
SwiftyGif(image: img)
.frame(width: w, height: w * img.size.height / img.size.width)
@@ -243,13 +243,13 @@ struct CIVideoView: View {
}
private func imageView(_ img: UIImage) -> some View {
let w = img.size.width <= img.size.height ? maxWidth * 0.75 : .infinity
let w = img.size.width <= img.size.height ? maxWidth * 0.75 : maxWidth
DispatchQueue.main.async { videoWidth = w }
return ZStack(alignment: .topTrailing) {
Image(uiImage: img)
.resizable()
.scaledToFit()
.frame(maxWidth: w)
.frame(width: w)
loadingIndicator()
}
}
+3 -1
View File
@@ -514,6 +514,7 @@ struct ChatView: View {
chat: chat,
chatItem: ci,
maxWidth: maxWidth,
itemWidth: maxWidth,
composeState: $composeState,
selectedMember: $selectedMember,
chatView: self
@@ -526,6 +527,7 @@ struct ChatView: View {
@ObservedObject var chat: Chat
var chatItem: ChatItem
var maxWidth: CGFloat
@State var itemWidth: CGFloat
@Binding var composeState: ComposeState
@Binding var selectedMember: GMember?
var chatView: ChatView
@@ -654,7 +656,7 @@ struct ChatView: View {
playbackState: $playbackState,
playbackTime: $playbackTime
)
.uiKitContextMenu(maxWidth: maxWidth, menu: uiMenu, allowMenu: $allowMenu)
.uiKitContextMenu(hasImageOrVideo: ci.content.msgContent?.isImageOrVideo == true, maxWidth: maxWidth, itemWidth: $itemWidth, menu: uiMenu, allowMenu: $allowMenu)
.accessibilityLabel("")
if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 {
chatItemReactions(ci)
@@ -11,11 +11,20 @@ import UIKit
import SwiftUI
extension View {
func uiKitContextMenu(maxWidth: CGFloat, menu: Binding<UIMenu>, allowMenu: Binding<Bool>) -> some View {
func uiKitContextMenu(hasImageOrVideo: Bool, maxWidth: CGFloat, itemWidth: Binding<CGFloat>, menu: Binding<UIMenu>, allowMenu: Binding<Bool>) -> some View {
Group {
if allowMenu.wrappedValue {
InteractionView(content: self, maxWidth: maxWidth, menu: menu)
.fixedSize(horizontal: true, vertical: false)
if hasImageOrVideo {
InteractionView(content:
self.environmentObject(ChatModel.shared)
.overlay(DetermineWidthImageVideoItem())
.onPreferenceChange(DetermineWidthImageVideoItem.Key.self) { itemWidth.wrappedValue = $0 == 0 ? maxWidth : $0 }
, maxWidth: maxWidth, itemWidth: itemWidth, menu: menu)
.frame(maxWidth: itemWidth.wrappedValue)
} else {
InteractionView(content: self.environmentObject(ChatModel.shared), maxWidth: maxWidth, itemWidth: itemWidth, menu: menu)
.fixedSize(horizontal: true, vertical: false)
}
} else {
self
}
@@ -31,13 +40,14 @@ private class HostingViewHolder: UIView {
struct InteractionView<Content: View>: UIViewRepresentable {
let content: Content
var maxWidth: CGFloat
var itemWidth: Binding<CGFloat>
@Binding var menu: UIMenu
func makeUIView(context: Context) -> UIView {
let view = HostingViewHolder()
view.contentSize = CGSizeMake(maxWidth, .infinity)
view.backgroundColor = .clear
let hostView = UIHostingController(rootView: content)
view.contentSize = hostView.view.intrinsicContentSize
hostView.view.translatesAutoresizingMaskIntoConstraints = false
let constraints = [
hostView.view.topAnchor.constraint(equalTo: view.topAnchor),
@@ -57,7 +67,11 @@ struct InteractionView<Content: View>: UIViewRepresentable {
}
func updateUIView(_ uiView: UIView, context: Context) {
(uiView as! HostingViewHolder).contentSize = uiView.subviews[0].sizeThatFits(CGSizeMake(maxWidth, .infinity))
let was = (uiView as! HostingViewHolder).contentSize
(uiView as! HostingViewHolder).contentSize = uiView.subviews[0].sizeThatFits(CGSizeMake(itemWidth.wrappedValue, .infinity))
if was != (uiView as! HostingViewHolder).contentSize {
uiView.invalidateIntrinsicContentSize()
}
}
func makeCoordinator() -> Coordinator {
@@ -21,6 +21,19 @@ struct DetermineWidth: View {
}
}
struct DetermineWidthImageVideoItem: View {
typealias Key = MaximumWidthImageVideoPreferenceKey
var body: some View {
GeometryReader { proxy in
Color.clear
.preference(
key: MaximumWidthImageVideoPreferenceKey.self,
value: proxy.size.width
)
}
}
}
struct MaximumWidthPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
@@ -28,6 +41,13 @@ struct MaximumWidthPreferenceKey: PreferenceKey {
}
}
struct MaximumWidthImageVideoPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
}
struct DetermineWidth_Previews: PreviewProvider {
static var previews: some View {
DetermineWidth()
@@ -45,11 +45,6 @@ struct DeveloperView: View {
}
if developerTools {
Section {
exportDebugProfileButton()
exportEventLogButton()
}
Section {
settingsRow("key") {
Toggle("Post-quantum E2EE", isOn: $pqExperimentalEnabled)
@@ -67,24 +62,6 @@ struct DeveloperView: View {
}
}
@ViewBuilder private func exportDebugProfileButton() -> some View {
let url = getAppDebugProfilePath()
settingsRow("square.and.arrow.up") {
Button("Export debugging profile") {
showShareSheet(items: [url])
}
}.disabled(!FileManager.default.fileExists(atPath: url.path))
}
@ViewBuilder private func exportEventLogButton() -> some View {
let url = getAppEventLogPath()
settingsRow("square.and.arrow.up") {
Button("Export event log") {
showShareSheet(items: [url])
}
}.disabled(!FileManager.default.fileExists(atPath: url.path))
}
private func setPQExperimentalEnabled(_ enable: Bool) {
do {
try apiSetPQEncryption(enable)
+51 -41
View File
@@ -65,20 +65,24 @@ class NSEThreads {
}
func processNotification(_ id: ChatId, _ ntf: NSENotification) async -> Void {
var timeoutOfWaiting: Int64 = 5_000_000000
while timeoutOfWaiting > 0 {
let activeThread = NSEThreads.queue.sync {
activeThreads.first(where: { (_, nse) in nse.receiveEntityId == id })
}
if activeThread?.1.processReceivedNtf?(ntf) == true {
var waitTime: Int64 = 5_000_000000
while waitTime > 0 {
if let (_, nse) = rcvEntityThread(id),
nse.shouldProcessNtf && nse.processReceivedNtf(ntf) {
break
} else {
try? await Task.sleep(nanoseconds: 10_000000)
timeoutOfWaiting -= 10_000000
waitTime -= 10_000000
}
}
}
private func rcvEntityThread(_ id: ChatId) -> (UUID, NotificationService)? {
NSEThreads.queue.sync {
activeThreads.first(where: { (_, nse) in nse.receiveEntityId == id })
}
}
func endThread(_ t: UUID) -> Bool {
NSEThreads.queue.sync {
let tActive: UUID? = if let index = activeThreads.firstIndex(where: { $0.0 == t }) {
@@ -113,9 +117,11 @@ class NotificationService: UNNotificationServiceExtension {
// thread is added to allThreads here - if thread did not start chat,
// chat does not need to be suspended but NSE state still needs to be set to "suspended".
var threadId: UUID? = NSEThreads.shared.newThread()
var notificationInfo: NtfMessages?
var receiveEntityId: String?
var expectedMessages: Set<String> = []
// return true if the message is taken - it prevents sending it to another NotificationService instance for processing
var processReceivedNtf: ((NSENotification) -> Bool)?
var shouldProcessNtf = false
var appSubscriber: AppSubscriber?
var returnedSuspension = false
@@ -192,39 +198,11 @@ class NotificationService: UNNotificationServiceExtension {
? .nse(createConnectionEventNtf(ntfInfo.user, connEntity))
: .empty
)
if let id = connEntity.id, let msgTs = ntfInfo.msgTs {
var expected = Set(ntfInfo.ntfMessages.map { $0.msgId })
processReceivedNtf = { ntf in
if !ntfInfo.user.showNotifications {
self.setBestAttemptNtf(.empty)
}
if case let .msgInfo(info) = ntf {
let found = expected.remove(info.msgId)
if found != nil {
logger.debug("NotificationService processNtf: msgInfo, last: \(expected.isEmpty)")
if expected.isEmpty {
self.deliverBestAttemptNtf()
}
return true
} else if info.msgTs > msgTs {
logger.debug("NotificationService processNtf: unexpected msgInfo, let other instance to process it, stopping this one")
self.deliverBestAttemptNtf()
return false
} else {
logger.debug("NotificationService processNtf: unknown message, let other instance to process it")
return false
}
} else if ntfInfo.user.showNotifications {
logger.debug("NotificationService processNtf: setting best attempt")
self.setBestAttemptNtf(ntf)
if ntf.isCallInvitation {
self.deliverBestAttemptNtf()
}
return true
}
return false
}
if let id = connEntity.id, ntfInfo.msgTs != nil {
notificationInfo = ntfInfo
receiveEntityId = id
expectedMessages = Set(ntfInfo.ntfMessages.map { $0.msgId })
shouldProcessNtf = true
return
}
}
@@ -240,6 +218,38 @@ class NotificationService: UNNotificationServiceExtension {
deliverBestAttemptNtf(urgent: true)
}
func processReceivedNtf(_ ntf: NSENotification) -> Bool {
guard let ntfInfo = notificationInfo, let msgTs = ntfInfo.msgTs else { return false }
if !ntfInfo.user.showNotifications {
self.setBestAttemptNtf(.empty)
}
if case let .msgInfo(info) = ntf {
let found = expectedMessages.remove(info.msgId)
if found != nil {
logger.debug("NotificationService processNtf: msgInfo, last: \(self.expectedMessages.isEmpty)")
if expectedMessages.isEmpty {
self.deliverBestAttemptNtf()
}
return true
} else if info.msgTs > msgTs {
logger.debug("NotificationService processNtf: unexpected msgInfo, let other instance to process it, stopping this one")
self.deliverBestAttemptNtf()
return false
} else {
logger.debug("NotificationService processNtf: unknown message, let other instance to process it")
return false
}
} else if ntfInfo.user.showNotifications {
logger.debug("NotificationService processNtf: setting best attempt")
self.setBestAttemptNtf(ntf)
if ntf.isCallInvitation {
self.deliverBestAttemptNtf()
}
return true
}
return false
}
func setBadgeCount() {
badgeCount = ntfBadgeCountGroupDefault.get() + 1
ntfBadgeCountGroupDefault.set(badgeCount)
@@ -262,7 +272,7 @@ class NotificationService: UNNotificationServiceExtension {
private func deliverBestAttemptNtf(urgent: Bool = false) {
logger.debug("NotificationService.deliverBestAttemptNtf")
// stop processing other messages
processReceivedNtf = nil
shouldProcessNtf = false
let suspend: Bool
if let t = threadId {
+66 -33
View File
@@ -36,11 +36,6 @@
5C35CFC827B2782E00FB6C6D /* BGManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C35CFC727B2782E00FB6C6D /* BGManager.swift */; };
5C35CFCB27B2E91D00FB6C6D /* NtfManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C35CFCA27B2E91D00FB6C6D /* NtfManager.swift */; };
5C36027327F47AD5009F19D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C36027227F47AD5009F19D9 /* AppDelegate.swift */; };
5C371E742BACC5D600100AD3 /* libHSsimplex-chat-5.6.0.4-E0iWSIg8fcR48og4na41Dv-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C371E6F2BACC5D600100AD3 /* libHSsimplex-chat-5.6.0.4-E0iWSIg8fcR48og4na41Dv-ghc9.6.3.a */; };
5C371E752BACC5D600100AD3 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C371E702BACC5D600100AD3 /* libgmpxx.a */; };
5C371E762BACC5D600100AD3 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C371E712BACC5D600100AD3 /* libffi.a */; };
5C371E772BACC5D600100AD3 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C371E722BACC5D600100AD3 /* libgmp.a */; };
5C371E782BACC5D600100AD3 /* libHSsimplex-chat-5.6.0.4-E0iWSIg8fcR48og4na41Dv.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C371E732BACC5D600100AD3 /* libHSsimplex-chat-5.6.0.4-E0iWSIg8fcR48og4na41Dv.a */; };
5C3A88CE27DF50170060F1C2 /* DetermineWidth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3A88CD27DF50170060F1C2 /* DetermineWidth.swift */; };
5C3A88D127DF57800060F1C2 /* FramedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3A88D027DF57800060F1C2 /* FramedItemView.swift */; };
5C3CCFCC2AE6BD3100C3F0C3 /* ConnectDesktopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3CCFCB2AE6BD3100C3F0C3 /* ConnectDesktopView.swift */; };
@@ -116,6 +111,11 @@
5CC2C0FC2809BF11000C35E3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FA2809BF11000C35E3 /* Localizable.strings */; };
5CC2C0FF2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FD2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings */; };
5CC868F329EB540C0017BBFD /* CIRcvDecryptionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */; };
5CC932F52BBDD9F9008A1EB6 /* libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC932F02BBDD9F9008A1EB6 /* libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto-ghc9.6.3.a */; };
5CC932F62BBDD9F9008A1EB6 /* libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC932F12BBDD9F9008A1EB6 /* libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto.a */; };
5CC932F72BBDD9FA008A1EB6 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC932F22BBDD9F9008A1EB6 /* libgmpxx.a */; };
5CC932F82BBDD9FA008A1EB6 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC932F32BBDD9F9008A1EB6 /* libgmp.a */; };
5CC932F92BBDD9FA008A1EB6 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC932F42BBDD9F9008A1EB6 /* libffi.a */; };
5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */; };
5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CD67B8D2B0E858A00C510B1 /* hs_init.h */; settings = {ATTRIBUTES = (Public, ); }; };
5CD67B902B0E858A00C510B1 /* hs_init.c in Sources */ = {isa = PBXBuildFile; fileRef = 5CD67B8E2B0E858A00C510B1 /* hs_init.c */; };
@@ -290,11 +290,6 @@
5C371E4E2BA9AAA200100AD3 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = "<group>"; };
5C371E4F2BA9AB6400100AD3 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = "hu.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
5C371E502BA9AB6400100AD3 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/InfoPlist.strings; sourceTree = "<group>"; };
5C371E6F2BACC5D600100AD3 /* libHSsimplex-chat-5.6.0.4-E0iWSIg8fcR48og4na41Dv-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.6.0.4-E0iWSIg8fcR48og4na41Dv-ghc9.6.3.a"; sourceTree = "<group>"; };
5C371E702BACC5D600100AD3 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5C371E712BACC5D600100AD3 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5C371E722BACC5D600100AD3 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5C371E732BACC5D600100AD3 /* libHSsimplex-chat-5.6.0.4-E0iWSIg8fcR48og4na41Dv.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.6.0.4-E0iWSIg8fcR48og4na41Dv.a"; sourceTree = "<group>"; };
5C3A88CD27DF50170060F1C2 /* DetermineWidth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetermineWidth.swift; sourceTree = "<group>"; };
5C3A88D027DF57800060F1C2 /* FramedItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FramedItemView.swift; sourceTree = "<group>"; };
5C3CCFCB2AE6BD3100C3F0C3 /* ConnectDesktopView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectDesktopView.swift; sourceTree = "<group>"; };
@@ -407,6 +402,11 @@
5CC2C0FB2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
5CC2C0FE2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = "ru.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIRcvDecryptionError.swift; sourceTree = "<group>"; };
5CC932F02BBDD9F9008A1EB6 /* libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto-ghc9.6.3.a"; sourceTree = "<group>"; };
5CC932F12BBDD9F9008A1EB6 /* libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto.a"; sourceTree = "<group>"; };
5CC932F22BBDD9F9008A1EB6 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5CC932F32BBDD9F9008A1EB6 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5CC932F42BBDD9F9008A1EB6 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5CCB939B297EFCB100399E78 /* NavStackCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavStackCompat.swift; sourceTree = "<group>"; };
5CD67B8D2B0E858A00C510B1 /* hs_init.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = hs_init.h; sourceTree = "<group>"; };
5CD67B8E2B0E858A00C510B1 /* hs_init.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = hs_init.c; sourceTree = "<group>"; };
@@ -521,12 +521,12 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
5C371E752BACC5D600100AD3 /* libgmpxx.a in Frameworks */,
5C371E742BACC5D600100AD3 /* libHSsimplex-chat-5.6.0.4-E0iWSIg8fcR48og4na41Dv-ghc9.6.3.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
5C371E782BACC5D600100AD3 /* libHSsimplex-chat-5.6.0.4-E0iWSIg8fcR48og4na41Dv.a in Frameworks */,
5C371E762BACC5D600100AD3 /* libffi.a in Frameworks */,
5C371E772BACC5D600100AD3 /* libgmp.a in Frameworks */,
5CC932F72BBDD9FA008A1EB6 /* libgmpxx.a in Frameworks */,
5CC932F62BBDD9F9008A1EB6 /* libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto.a in Frameworks */,
5CC932F52BBDD9F9008A1EB6 /* libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto-ghc9.6.3.a in Frameworks */,
5CC932F92BBDD9FA008A1EB6 /* libffi.a in Frameworks */,
5CC932F82BBDD9FA008A1EB6 /* libgmp.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -590,11 +590,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup;
children = (
5C371E712BACC5D600100AD3 /* libffi.a */,
5C371E722BACC5D600100AD3 /* libgmp.a */,
5C371E702BACC5D600100AD3 /* libgmpxx.a */,
5C371E6F2BACC5D600100AD3 /* libHSsimplex-chat-5.6.0.4-E0iWSIg8fcR48og4na41Dv-ghc9.6.3.a */,
5C371E732BACC5D600100AD3 /* libHSsimplex-chat-5.6.0.4-E0iWSIg8fcR48og4na41Dv.a */,
5CC932F42BBDD9F9008A1EB6 /* libffi.a */,
5CC932F32BBDD9F9008A1EB6 /* libgmp.a */,
5CC932F22BBDD9F9008A1EB6 /* libgmpxx.a */,
5CC932F02BBDD9F9008A1EB6 /* libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto-ghc9.6.3.a */,
5CC932F12BBDD9F9008A1EB6 /* libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto.a */,
);
path = Libraries;
sourceTree = "<group>";
@@ -1441,6 +1441,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
@@ -1502,6 +1503,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -1530,12 +1532,16 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CLANG_ENABLE_MODULES = YES;
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 204;
CURRENT_PROJECT_VERSION = 206;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
GCC_OPTIMIZATION_LEVEL = s;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "SimpleX--iOS--Info.plist";
INFOPLIST_KEY_NSCameraUsageDescription = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
@@ -1554,11 +1560,13 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 5.6;
LLVM_LTO = YES_THIN;
MARKETING_VERSION = 5.6.1;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
@@ -1573,12 +1581,16 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CLANG_ENABLE_MODULES = YES;
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 204;
CURRENT_PROJECT_VERSION = 206;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
ENABLE_CODE_COVERAGE = NO;
ENABLE_PREVIEWS = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "SimpleX--iOS--Info.plist";
INFOPLIST_KEY_NSCameraUsageDescription = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
@@ -1597,7 +1609,8 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 5.6;
LLVM_LTO = YES;
MARKETING_VERSION = 5.6.1;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1653,12 +1666,15 @@
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 204;
CURRENT_PROJECT_VERSION = 206;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GCC_OPTIMIZATION_LEVEL = s;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "SimpleX NSE/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "SimpleX NSE";
@@ -1669,13 +1685,15 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 5.6;
LLVM_LTO = YES;
MARKETING_VERSION = 5.6.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Osize";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
@@ -1685,12 +1703,15 @@
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 204;
CURRENT_PROJECT_VERSION = 206;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_CODE_COVERAGE = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "SimpleX NSE/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "SimpleX NSE";
@@ -1701,13 +1722,15 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 5.6;
LLVM_LTO = YES;
MARKETING_VERSION = 5.6.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Osize";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
VALIDATE_PRODUCT = YES;
@@ -1719,14 +1742,17 @@
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 204;
CURRENT_PROJECT_VERSION = 206;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
ENABLE_BITCODE = NO;
GCC_OPTIMIZATION_LEVEL = s;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 SimpleX Chat. All rights reserved.";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
@@ -1744,7 +1770,8 @@
"$(inherited)",
"$(PROJECT_DIR)/Libraries/sim",
);
MARKETING_VERSION = 5.6;
LLVM_LTO = YES;
MARKETING_VERSION = 5.6.1;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
@@ -1753,6 +1780,7 @@
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_INCLUDE_PATHS = "";
SWIFT_OBJC_BRIDGING_HEADER = ./SimpleXChat/SimpleX.h;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
@@ -1765,14 +1793,17 @@
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 204;
CURRENT_PROJECT_VERSION = 206;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
ENABLE_BITCODE = NO;
ENABLE_CODE_COVERAGE = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 SimpleX Chat. All rights reserved.";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
@@ -1790,7 +1821,8 @@
"$(inherited)",
"$(PROJECT_DIR)/Libraries/sim",
);
MARKETING_VERSION = 5.6;
LLVM_LTO = YES;
MARKETING_VERSION = 5.6.1;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
@@ -1799,6 +1831,7 @@
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_INCLUDE_PATHS = "";
SWIFT_OBJC_BRIDGING_HEADER = ./SimpleXChat/SimpleX.h;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
+8
View File
@@ -3209,6 +3209,14 @@ public enum MsgContent: Equatable {
}
}
public var isImageOrVideo: Bool {
switch self {
case .image: true
case .video: true
default: false
}
}
var cmdString: String {
"json \(encodeJSON(self))"
}
-16
View File
@@ -52,10 +52,6 @@ func getAppDirectory() -> URL {
let DB_FILE_PREFIX = "simplex_v1"
let DEBUG_PROFILE_PREFIX = "simplex_debug"
let DEBUG_PROFILE_EXTENSION = ".hp"
func getLegacyDatabasePath() -> URL {
getDocumentsDirectory().appendingPathComponent("mobile_v1", isDirectory: false)
}
@@ -66,18 +62,6 @@ public func getAppDatabasePath() -> URL {
: getLegacyDatabasePath()
}
public func getAppDebugProfilePrefixPath() -> URL {
getAppDirectory().appendingPathComponent(DEBUG_PROFILE_PREFIX, isDirectory: false)
}
public func getAppDebugProfilePath() -> URL {
getAppDirectory().appendingPathComponent(DEBUG_PROFILE_PREFIX + DEBUG_PROFILE_EXTENSION, isDirectory: false)
}
public func getAppEventLogPath() -> URL {
getAppDirectory().appendingPathComponent("simplex.eventlog", isDirectory: false)
}
func fileModificationDate(_ path: String) -> Date? {
do {
let attr = try FileManager.default.attributesOfItem(atPath: path)
+10 -27
View File
@@ -7,36 +7,19 @@
//
#include "hs_init.h"
#include <string.h>
extern void hs_init_with_rtsopts(int * argc, char **argv[]);
void haskell_init(const char *eventlog, const char *heap_profile) {
// setup static arena for bump allocation and passing to RTS
char *argv[32] = {0,};
int argc = 0; // number of arguments used so far, always stands at the first NULL in argv
// common args
argv[argc++] = "simplex"; // fake program name
argv[argc++] = "+RTS"; // start adding RTS options
argv[argc++] = "-T"; // make GC counters available from inside the program
argv[argc++] = "-A64m"; // chunk size for new allocations (less frequent GC)
argv[argc++] = "-H64m"; // larger heap size on start (faster boot)
// argv[argc++] = "-M8G"; // keep memory usage under 8G, collecting more aggressively when approaching it (and crashing sooner rather than taking down the whole system)
if (eventlog) {
static char ol[1024] = "-ol";
(void)strncpy(&ol[3], eventlog, sizeof(ol) - 3);
argv[argc++] = ol;
argv[argc++] = "-l-agu"; // collect GC and user events
}
if (heap_profile) {
static char po[1024] = "-po";
(void)strncpy(&po[3], heap_profile, sizeof(po) - 3);
argv[argc++] = po; // adds ".hp" extension
argv[argc++] = "-hT"; // emit heap profile by closure type
}
int non_moving_gc = !heap_profile; // not compatible with heap profile
if (non_moving_gc) argv[argc++] = "-xn";
// wrap args as expected by RTS
void haskell_init(void) {
int argc = 5;
char *argv[] = {
"simplex",
"+RTS", // requires `hs_init_with_rtsopts`
"-A64m", // chunk size for new allocations
"-H64m", // initial heap size
"-xn", // non-moving GC
0
};
char **pargv = argv;
hs_init_with_rtsopts(&argc, &pargv);
}
+1 -1
View File
@@ -9,7 +9,7 @@
#ifndef hs_init_h
#define hs_init_h
void haskell_init(const char *eventlog, const char *heap_profile);
void haskell_init(void);
void haskell_init_nse(void);
@@ -6,7 +6,6 @@ import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import chat.simplex.common.helpers.toUri
import chat.simplex.common.model.CIFile
import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.ModalManager
@@ -15,7 +14,6 @@ import coil.compose.rememberAsyncImagePainter
import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import coil.request.ImageRequest
import java.net.URI
@Composable
actual fun SimpleAndAnimatedImageView(
@@ -43,6 +41,7 @@ actual fun SimpleAndAnimatedImageView(
}
private val imageLoader = ImageLoader.Builder(androidAppContext)
.networkObserverEnabled(false)
.components {
if (SDK_INT >= 28) {
add(ImageDecoderDecoder.Factory())
@@ -3,7 +3,7 @@ package chat.simplex.common.views.chat.item
import android.os.Build
import android.view.View
import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.painter.BitmapPainter
@@ -11,8 +11,8 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.isVisible
import chat.simplex.common.helpers.toUri
import chat.simplex.common.platform.VideoPlayer
import chat.simplex.common.platform.androidAppContext
import chat.simplex.res.MR
import coil.ImageLoader
import coil.compose.rememberAsyncImagePainter
@@ -23,21 +23,11 @@ import coil.size.Size
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
import com.google.android.exoplayer2.ui.StyledPlayerView
import dev.icerock.moko.resources.compose.stringResource
import java.net.URI
@Composable
actual fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap: ImageBitmap) {
// I'm making a new instance of imageLoader here because if I use one instance in multiple places
// I'm using a new private instance of imageLoader here because if I use one instance in multiple places
// after end of composition here a GIF from the first instance will be paused automatically which isn't what I want
val imageLoader = ImageLoader.Builder(LocalContext.current)
.components {
if (Build.VERSION.SDK_INT >= 28) {
add(ImageDecoderDecoder.Factory())
} else {
add(GifDecoder.Factory())
}
}
.build()
Image(
rememberAsyncImagePainter(
ImageRequest.Builder(LocalContext.current).data(data = data).size(Size.ORIGINAL).build(),
@@ -73,3 +63,14 @@ actual fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier, close: (
modifier
)
}
private val imageLoader = ImageLoader.Builder(androidAppContext)
.networkObserverEnabled(false)
.components {
if (Build.VERSION.SDK_INT >= 28) {
add(ImageDecoderDecoder.Factory())
} else {
add(GifDecoder.Factory())
}
}
.build()
@@ -1392,11 +1392,12 @@ private fun providerForGallery(
return null
}
var initialIndex = Int.MAX_VALUE / 2
// Pager has a bug with overflowing when total pages is around Int.MAX_VALUE. Using smaller value
var initialIndex = 10000 / 2
var initialChatId = cItemId
return object: ImageGalleryProvider {
override val initialIndex: Int = initialIndex
override val totalMediaSize = mutableStateOf(Int.MAX_VALUE)
override val totalMediaSize = mutableStateOf(10000)
override fun getMedia(index: Int): ProviderMedia? {
val internalIndex = initialIndex - index
val item = item(internalIndex, initialChatId)?.second ?: return null
@@ -38,9 +38,7 @@ import chat.simplex.common.views.usersettings.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.runBlocking
@Composable
fun ConnectMobileView() {
@@ -269,12 +267,20 @@ fun AddingMobileDevice(showTitle: Boolean, staleQrCode: MutableState<Boolean>, c
var cachedR by remember { mutableStateOf<CR.RemoteHostStarted?>(null) }
val customAddress = rememberSaveable { mutableStateOf<RemoteCtrlAddress?>(null) }
val customPort = rememberSaveable { mutableStateOf<Int?>(null) }
var userChangedAddress by rememberSaveable { mutableStateOf(false) }
var userChangedPort by rememberSaveable { mutableStateOf(false) }
val startRemoteHost = suspend {
if (customAddress.value != cachedR.address && cachedR != null) {
userChangedAddress = true
}
if (customPort.value != cachedR.port && cachedR != null) {
userChangedPort = true
}
val r = chatModel.controller.startRemoteHost(
rhId = null,
multicast = controller.appPrefs.offerRemoteMulticast.get(),
address = if (customAddress.value?.address != cachedR.address?.address) customAddress.value else cachedR.rh?.bindAddress_,
port = if (customPort.value != cachedR.port) customPort.value else cachedR.rh?.bindPort_
address = if (customAddress.value != null && userChangedAddress) customAddress.value else cachedR.rh?.bindAddress_,
port = if (customPort.value != null && userChangedPort) customPort.value else cachedR.rh?.bindPort_
)
if (r != null) {
cachedR = r
@@ -343,12 +349,20 @@ private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState
var cachedR by remember { mutableStateOf<CR.RemoteHostStarted?>(null) }
val customAddress = rememberSaveable { mutableStateOf<RemoteCtrlAddress?>(null) }
val customPort = rememberSaveable { mutableStateOf<Int?>(null) }
var userChangedAddress by rememberSaveable { mutableStateOf(false) }
var userChangedPort by rememberSaveable { mutableStateOf(false) }
val startRemoteHost = suspend {
if (customAddress.value != cachedR.address && cachedR != null) {
userChangedAddress = true
}
if (customPort.value != cachedR.port && cachedR != null) {
userChangedPort = true
}
val r = chatModel.controller.startRemoteHost(
rhId = rh.remoteHostId,
multicast = controller.appPrefs.offerRemoteMulticast.get(),
address = if (customAddress.value?.address != cachedR.address?.address) customAddress.value else cachedR.rh?.bindAddress_ ?: rh.bindAddress_,
port = if (customPort.value != cachedR.port) customPort.value else cachedR.rh?.bindPort_ ?: rh.bindPort_
address = if (customAddress.value != null && userChangedAddress) customAddress.value else cachedR.rh?.bindAddress_ ?: rh.bindAddress_,
port = if (customPort.value != null && userChangedPort) customPort.value else cachedR.rh?.bindPort_ ?: rh.bindPort_
)
if (r != null) {
cachedR = r
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="120"
height="120"
viewBox="121 0 40 40"
fill="none"
version="1.1"
id="svg3"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="m 126.52238,11.425398 5.80302,5.716401 5.88962,-5.889626 2.8582,2.858201 L 135.1836,20 l 5.7164,5.716402 -2.94482,2.8582 -5.7164,-5.629789 -5.88962,5.803014 -2.8582,-2.858201 5.88962,-5.803014 -5.803,-5.716402 z"
fill="#030749"
id="path1"
style="stroke-width:0.866122" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="m 137.86858,28.661214 2.94481,-2.944812 v 0 l 5.88963,-5.803014 -5.80302,-5.62979 v 0 l -2.8582,-2.8582 -5.7164,-5.7164023 2.94481,-2.9448129 5.7164,5.7164017 5.88963,-5.8030138 2.8582,2.8582008 -5.88962,5.8030145 5.7164,5.716401 5.88962,-5.803014 2.8582,2.858201 -5.88962,5.803014 5.803,5.716402 -2.9448,2.858201 -5.7164,-5.716402 -5.88963,5.803013 5.7164,5.716402 -2.8582,2.944813 -5.80301,-5.716402 -5.80302,5.803015 -2.8582,-2.858201 z"
fill="url(#paint0_linear_40_164)"
id="path2"
style="fill:url(#paint0_linear_40_164);stroke-width:0.866122" />
<defs
id="defs3">
<linearGradient
x1="135.948"
y1="-0.81632602"
x2="132.09599"
y2="36.985699"
gradientUnits="userSpaceOnUse"
id="paint0_linear_40_164"
gradientTransform="matrix(0.86612147,0,0,0.86612147,18.863485,2.6775707)">
<stop
stop-color="#01f1ff"
id="stop2" />
<stop
offset="1"
stop-color="#0197ff"
id="stop3" />
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -21,6 +21,7 @@ import chat.simplex.common.ui.theme.SimpleXTheme
import chat.simplex.common.views.TerminalView
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.*
import java.awt.event.WindowEvent
@@ -103,7 +104,7 @@ private fun ApplicationScope.AppWindow(closedByError: MutableState<Boolean>) {
simplexWindowState.windowState = windowState
// Reload all strings in all @Composable's after language change at runtime
if (remember { ChatController.appPrefs.appLanguage.state }.value != "") {
Window(state = windowState, onCloseRequest = { closedByError.value = false; exitApplication() }, onKeyEvent = {
Window(state = windowState, icon = painterResource(MR.images.ic_simplex), onCloseRequest = { closedByError.value = false; exitApplication() }, onKeyEvent = {
if (it.key == Key.Escape && it.type == KeyEventType.KeyUp) {
simplexWindowState.backstack.lastOrNull()?.invoke() != null
} else {
@@ -7,6 +7,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import chat.simplex.common.platform.*
import chat.simplex.common.simplexWindowState
import java.awt.Window
@Composable
actual fun PlayerView(player: VideoPlayer, width: Dp, onClick: () -> Unit, onLongClick: () -> Unit, stop: () -> Unit) {
@@ -23,14 +25,15 @@ actual fun PlayerView(player: VideoPlayer, width: Dp, onClick: () -> Unit, onLon
}
}
/*
* This function doesn't take into account multi-window environment. In case more windows will be used, modify the code
* */
@Composable
actual fun LocalWindowWidth(): Dp {
return with(LocalDensity.current) { (java.awt.Window.getWindows().find { it.isActive }?.width ?: 0).toDp() }
/*val density = LocalDensity.current
var width by remember { mutableStateOf(with(density) { (java.awt.Window.getWindows().find { it.isActive }?.width ?: 0).toDp() }) }
SideEffect {
if (width != with(density) { (java.awt.Window.getWindows().find { it.isActive }?.width ?: 0).toDp() })
width = with(density) { (java.awt.Window.getWindows().find { it.isActive }?.width ?: 0).toDp() }
actual fun LocalWindowWidth(): Dp = with(LocalDensity.current) {
val windows = java.awt.Window.getWindows()
if (windows.size == 1) {
(windows.getOrNull(0)?.width ?: 0).toDp()
} else {
simplexWindowState.windowState.size.width
}
return width.also { println("LALAL $it") }*/
}
+4 -4
View File
@@ -25,11 +25,11 @@ android.nonTransitiveRClass=true
android.enableJetifier=true
kotlin.mpp.androidSourceSetLayoutVersion=2
android.version_name=5.6
android.version_code=191
android.version_name=5.6.1
android.version_code=193
desktop.version_name=5.6
desktop.version_code=35
desktop.version_name=5.6.1
desktop.version_code=36
kotlin.version=1.9.23
gradle.plugin.version=8.2.0
+1 -7
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: ee90ea6a69fe8283d37d9821cd83798fd0a76260
tag: 6bc4f6c94e11f59604b0d9c576e62e01bc08b4cd
source-repository-package
type: git
@@ -34,12 +34,6 @@ source-repository-package
location: https://github.com/simplex-chat/aeson.git
tag: aab7b5a14d6c5ea64c64dcaee418de1bb00dcc2b
-- old bs/text compat for 8.10
source-repository-package
type: git
location: https://github.com/simplex-chat/base64.git
tag: 2d77b6dbcaffc00570a70be8694049f3710e7c94
source-repository-package
type: git
location: https://github.com/simplex-chat/haskell-terminal.git
+72
View File
@@ -0,0 +1,72 @@
# Large public grups / channels
## Background
SimpleX Chat users participate in public groups that were created for small, fully connected p2p groups - working groups, teams, etc. The ability to join the groups via the links was added as an afterthought, without forward looking design, simply to accomodate the interest from the users to use SimpleX platform for public groups and communities. Overall, it's correct to say that the emergence of public groups was unexpected in the context of private messaging, and it shows that protecting participants and publishers identity, and having per-group identity is important for many people.
## Problems of the current p2p design
### It doesn't scale to large size
Current design assumes that each peer is connected to each peer and sends messages to all. It creates non-trivial cost of establishing the connections as the group grows, some abandoned connections when some members remain in "connecting" state and also linearly growing traffic to send each message.
Historically, there were p2p designs when peers connected not to all but some members, but they were mostly used by desktop clients with more persistent network connections, did not provide any asynchronous delivery and were trading lower traffic for latency and availability. Such designs were viable for file sharing across desktop devices, but it probably would not work well for dynamic real-time communities with active participation from a small share of members and the design of other members to observe the conversation as it happens.
### It doesn't account for participation asymmetry
Most members of large public groups do not send messages, so connecting them to all other members directly appears unnecessary and costly, and it also requires tracking when members became inactive to stop sending messages to them.
## Objectives for the new design
### Transcript integrity
This issue is covered in detail in [Group integrity](./2023-10-20-group-integrity.md).
### Asynchronous delivery to group
Asynchronous delivery is important to protect participants privacy from traffic observation. In addition to that, sending scheduled posts is quite often a convenient feature to schedule multiple updates for a longer period of time - for example, schedule daily updates for a week, doing it once a week.
### Ability to conceal members list
This is a rather common request for the current groups, and while it could be possible of course to hide it on the client level, this still makes data available via the database, and puts less technical users in unfair position, while not protecting users privacy from technically competent members.
### Support pre-moderation
As the group size grows, so does the activity of the bad actors. Some groups will benefit from switching all, some, most, or new members to pre-moderation - when each post needs to be approved by admin before it becomes visible to all members. It would slow down the conversations, but it would allow a better content quality and owners' control of the content.
## Channels based on super-peers
The proposal is to model the new UX design from Telegram channels, with optional subchannels, and granular participation rights for the members / followers.
Another consideration is to create democratically governed communities when creators don't own the community but only appoint the initial administrators, but as the community grows it can elect the new admins or moderators from the existing members, where voting power is somehow determined by the community score (which is necessary to compensate for anonymous participants who could subvert the vote if plain vote count was made). This is probably out of scope for the initial implementation, but this idea is very appealing and it doesn't exist in any other decentralised platforms.
Technologically, the channel or group would determine which super-peers would host the group or channel, with group content being a merkle tree with ability to remove some content creating holes - which seems to be very important quality, both to remove undesirable content and to protect participants privacy.
Super-peer would manage this merkle-tree state based on the messages from owners, admins and members, with the ability to make some destructive actions confirmed by more than one command. E.g., group/channel deletion may require at least 2 or 3 votes (respectively, for 3 and 5 owners), thus protecting both from accidental deletions and from attacks via owner - one of the owners being compromised won't result in group deletion if 2 votes are required. As the group size grows, owners can also modify rules (which in itself can also require m of n votes).
## Joining group
The current model when the link to join the group is, effectively, an address of one of the admins, is not censorship-resistant, reliable or convenient - the admin can be offline, be removed, etc. So we want to somehow include addresses of multiple super-peers to join the group. Without identity-layer, the addresses are quite large already, and including multiple addresses in one link, while possible, would make the qr code very hard to scan. Practically, without creating identity layer, we can use up to 2-3 super-peer addresses for the group, and increase it later. 2 addresses is likely to be satisfactory, as one of them could be super-peer hosted by SimpleX Chat, and another - by group owners.
The client then will be connecting to all super-peers in the address. Once connected, these super-peers could send the addresses of the additional super-peers, but this is probably unnecessary for the initial release.
## MVP
The challenge is to decide what should be in scope for the initial release, to make it a valuable upgrade and a viable starting point, without overloading it with the functions that can be added later.
MVP scope:
- Core functioning of the group - creation/deletion/choosing and changing super-peers. While a large scope, it appears essential.
- Message delivery via super-peers.
- Super-peer protocol extension. Most likely super-peer would receive ordinary chat messages, but some operations should be added and require additional protocol messages - adding/removing super-peers to groups.
- Protocol extensions for owner actions with approvals - we already had several accidental deletions or lost owner accounts. Possibly, it is out of MVP scope.
- Search and history navigation. Current decision to send 100 messages both creates unnecessary traffic spikes, and also doesn't provide access to older history and search functions. But, possibly, it should also be in follow up improvements, and only should be included as the initial protocol design.
- New format of the group address to include more than one super-peer.
- Granular permissions and management model. While the user interface can evolve, the protocol and the scenarios, and also rules models seems better to be added from the beginning.
Follow-up / improvements:
- More scalable client - we already observe scalability issues with directory service, so replacing SQLite with Postgres, if the group participation starts growing seems very important.
Out of scope:
- additional super-peers.
- smart-contacts. While very tempting to generalise permissions and management model via smart contracts, that would radically increase complexity and delivery time.
+6 -1
View File
@@ -1,5 +1,5 @@
name: simplex-chat
version: 5.6.0.4
version: 5.6.1.1
#synopsis:
#description:
homepage: https://github.com/simplex-chat/simplex-chat#readme
@@ -18,6 +18,7 @@ dependencies:
- async == 2.2.*
- attoparsec == 0.14.*
- base >= 4.7 && < 5
- base64-bytestring >= 1.0 && < 1.3
- composition == 1.0.*
- constraints >= 0.12 && < 0.14
- containers == 0.6.*
@@ -150,6 +151,7 @@ tests:
ghc-options:
# - -haddock
- -O2
- -Wall
- -Wcompat
- -Werror=incomplete-patterns
@@ -157,3 +159,6 @@ ghc-options:
- -Wincomplete-record-updates
- -Wincomplete-uni-patterns
- -Wunused-type-patterns
default-extensions:
- StrictData
+1 -2
View File
@@ -1,10 +1,9 @@
{
"https://github.com/simplex-chat/simplexmq.git"."ee90ea6a69fe8283d37d9821cd83798fd0a76260" = "0my9f4dlfa79yq73rys0m2zb61fd9bp65djvavk6jwy6qzl5vr40";
"https://github.com/simplex-chat/simplexmq.git"."6bc4f6c94e11f59604b0d9c576e62e01bc08b4cd" = "08l00ay1ibz7skhlpfjp6z2821zpfd0kxplwdm6zc63m2f6za7cv";
"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";
"https://github.com/simplex-chat/aeson.git"."aab7b5a14d6c5ea64c64dcaee418de1bb00dcc2b" = "0jz7kda8gai893vyvj96fy962ncv8dcsx71fbddyy8zrvc88jfrr";
"https://github.com/simplex-chat/base64.git"."2d77b6dbcaffc00570a70be8694049f3710e7c94" = "0zdskk67fzqrrx1i29s3shp7fh9c0krmq5h6hq03qx0n3xy2m44b";
"https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj";
"https://github.com/simplex-chat/android-support.git"."9aa09f148089d6752ce563b14c2df1895718d806" = "0pbf2pf13v2kjzi397nr13f1h3jv0imvsq8rpiyy2qyx5vd50pqn";
"https://github.com/simplex-chat/zip.git"."bd421c6b19cc4c465cd7af1f6f26169fb8ee1ebc" = "1csqfjhvc8wb5h4kxxndmb6iw7b4ib9ff2n81hrizsmnf45a6gg0";
+29 -8
View File
@@ -5,7 +5,7 @@ cabal-version: 1.12
-- see: https://github.com/sol/hpack
name: simplex-chat
version: 5.6.0.4
version: 5.6.1.1
category: Web, System, Services, Cryptography
homepage: https://github.com/simplex-chat/simplex-chat#readme
author: simplex.chat
@@ -181,13 +181,16 @@ library
Paths_simplex_chat
hs-source-dirs:
src
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns
default-extensions:
StrictData
ghc-options: -O2 -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns
build-depends:
aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12
, async ==2.2.*
, attoparsec ==0.14.*
, base >=4.7 && <5
, base64-bytestring >=1.0 && <1.3
, composition ==1.0.*
, constraints >=0.12 && <0.14
, containers ==0.6.*
@@ -240,13 +243,16 @@ executable simplex-bot
Paths_simplex_chat
hs-source-dirs:
apps/simplex-bot
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded
default-extensions:
StrictData
ghc-options: -O2 -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded
build-depends:
aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12
, async ==2.2.*
, attoparsec ==0.14.*
, base >=4.7 && <5
, base64-bytestring >=1.0 && <1.3
, composition ==1.0.*
, constraints >=0.12 && <0.14
, containers ==0.6.*
@@ -300,13 +306,16 @@ executable simplex-bot-advanced
Paths_simplex_chat
hs-source-dirs:
apps/simplex-bot-advanced
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded
default-extensions:
StrictData
ghc-options: -O2 -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded
build-depends:
aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12
, async ==2.2.*
, attoparsec ==0.14.*
, base >=4.7 && <5
, base64-bytestring >=1.0 && <1.3
, composition ==1.0.*
, constraints >=0.12 && <0.14
, containers ==0.6.*
@@ -359,17 +368,20 @@ executable simplex-broadcast-bot
hs-source-dirs:
apps/simplex-broadcast-bot
apps/simplex-broadcast-bot/src
default-extensions:
StrictData
other-modules:
Broadcast.Bot
Broadcast.Options
Paths_simplex_chat
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded
ghc-options: -O2 -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded
build-depends:
aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12
, async ==2.2.*
, attoparsec ==0.14.*
, base >=4.7 && <5
, base64-bytestring >=1.0 && <1.3
, composition ==1.0.*
, constraints >=0.12 && <0.14
, containers ==0.6.*
@@ -424,13 +436,16 @@ executable simplex-chat
Paths_simplex_chat
hs-source-dirs:
apps/simplex-chat
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded
default-extensions:
StrictData
ghc-options: -O2 -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded
build-depends:
aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12
, async ==2.2.*
, attoparsec ==0.14.*
, base >=4.7 && <5
, base64-bytestring >=1.0 && <1.3
, composition ==1.0.*
, constraints >=0.12 && <0.14
, containers ==0.6.*
@@ -484,6 +499,8 @@ executable simplex-directory-service
hs-source-dirs:
apps/simplex-directory-service
apps/simplex-directory-service/src
default-extensions:
StrictData
other-modules:
Directory.Events
Directory.Options
@@ -491,13 +508,14 @@ executable simplex-directory-service
Directory.Service
Directory.Store
Paths_simplex_chat
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded
ghc-options: -O2 -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded
build-depends:
aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12
, async ==2.2.*
, attoparsec ==0.14.*
, base >=4.7 && <5
, base64-bytestring >=1.0 && <1.3
, composition ==1.0.*
, constraints >=0.12 && <0.14
, containers ==0.6.*
@@ -582,7 +600,9 @@ test-suite simplex-chat-test
tests
apps/simplex-broadcast-bot/src
apps/simplex-directory-service/src
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded
default-extensions:
StrictData
ghc-options: -O2 -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded
build-depends:
QuickCheck ==2.14.*
, aeson ==2.2.*
@@ -590,6 +610,7 @@ test-suite simplex-chat-test
, async ==2.2.*
, attoparsec ==0.14.*
, base >=4.7 && <5
, base64-bytestring >=1.0 && <1.3
, composition ==1.0.*
, constraints >=0.12 && <0.14
, containers ==0.6.*
+485 -482
View File
File diff suppressed because it is too large Load Diff
+13 -13
View File
@@ -44,7 +44,7 @@ archiveChatDbFile = "simplex_v1_chat.db"
archiveFilesFolder :: String
archiveFilesFolder = "simplex_v1_files"
exportArchive :: ChatMonad m => ArchiveConfig -> m ()
exportArchive :: ArchiveConfig -> CM' ()
exportArchive cfg@ArchiveConfig {archivePath, disableCompression} =
withTempDir cfg "simplex-chat." $ \dir -> do
StorageFiles {chatStore, agentStore, filesPath} <- storageFiles
@@ -55,7 +55,7 @@ exportArchive cfg@ArchiveConfig {archivePath, disableCompression} =
let method = if disableCompression == Just True then Z.Store else Z.Deflate
Z.createArchive archivePath $ Z.packDirRecur method Z.mkEntrySelector dir
importArchive :: ChatMonad m => ArchiveConfig -> m [ArchiveError]
importArchive :: ArchiveConfig -> CM' [ArchiveError]
importArchive cfg@ArchiveConfig {archivePath} =
withTempDir cfg "simplex-chat." $ \dir -> do
Z.withArchive archivePath $ Z.unpackInto dir
@@ -78,12 +78,12 @@ importArchive cfg@ArchiveConfig {archivePath} =
(pure [])
_ -> pure []
withTempDir :: ChatMonad m => ArchiveConfig -> (String -> (FilePath -> m a) -> m a)
withTempDir :: ArchiveConfig -> (String -> (FilePath -> CM' a) -> CM' a)
withTempDir cfg = case parentTempDirectory (cfg :: ArchiveConfig) of
Just tmpDir -> withTempDirectory tmpDir
_ -> withSystemTempDirectory
copyDirectoryFiles :: ChatMonad m => FilePath -> FilePath -> m [ArchiveError]
copyDirectoryFiles :: FilePath -> FilePath -> CM' [ArchiveError]
copyDirectoryFiles fromDir toDir = do
createDirectoryIfMissing False toDir
fs <- listDirectory fromDir
@@ -97,9 +97,9 @@ copyDirectoryFiles fromDir toDir = do
f' = fromDir </> fn
whenM (doesFileExist f') $ copyFile f' $ toDir </> fn
deleteStorage :: ChatMonad m => m ()
deleteStorage :: CM ()
deleteStorage = do
fs <- storageFiles
fs <- lift storageFiles
liftIO $ closeSQLiteStore `withStores` fs
remove `withDBs` fs
mapM_ removeDir $ filesPath fs
@@ -114,17 +114,17 @@ data StorageFiles = StorageFiles
filesPath :: Maybe FilePath
}
storageFiles :: ChatMonad m => m StorageFiles
storageFiles :: CM' StorageFiles
storageFiles = do
ChatController {chatStore, filesFolder, smpAgent} <- ask
let agentStore = agentClientStore smpAgent
filesPath <- readTVarIO filesFolder
pure StorageFiles {chatStore, agentStore, filesPath}
sqlCipherExport :: forall m. ChatMonad m => DBEncryptionConfig -> m ()
sqlCipherExport :: DBEncryptionConfig -> CM ()
sqlCipherExport DBEncryptionConfig {currentKey = DBEncryptionKey key, newKey = DBEncryptionKey key', keepKey} =
when (key /= key') $ do
fs <- storageFiles
fs <- lift storageFiles
checkFile `withDBs` fs
backup `withDBs` fs
checkEncryption `withStores` fs
@@ -159,7 +159,7 @@ sqlCipherExport DBEncryptionConfig {currentKey = DBEncryptionKey key, newKey = D
"DETACH DATABASE exported;"
]
withDB :: forall a m. ChatMonad m => FilePath -> (SQL.Database -> IO a) -> (SQLiteError -> DatabaseError) -> m ()
withDB :: FilePath -> (SQL.Database -> IO a) -> (SQLiteError -> DatabaseError) -> CM ()
withDB f' a err =
liftIO (bracket (SQL.open $ T.pack f') SQL.close a $> Nothing)
`catch` checkSQLError
@@ -169,7 +169,7 @@ withDB f' a err =
checkSQLError e = case SQL.sqlError e of
SQL.ErrorNotADatabase -> pure $ Just SQLiteErrorNotADatabase
_ -> sqliteError' e
sqliteError' :: Show e => e -> m (Maybe SQLiteError)
sqliteError' :: Show e => e -> CM (Maybe SQLiteError)
sqliteError' = pure . Just . SQLiteError . show
testSQL :: BA.ScrubbedBytes -> Text
@@ -184,9 +184,9 @@ testSQL k =
keySQL :: BA.ScrubbedBytes -> [Text]
keySQL k = ["PRAGMA key = " <> keyString k <> ";" | not (BA.null k)]
sqlCipherTestKey :: forall m. ChatMonad m => DBEncryptionKey -> m ()
sqlCipherTestKey :: DBEncryptionKey -> CM ()
sqlCipherTestKey (DBEncryptionKey key) = do
fs <- storageFiles
fs <- lift storageFiles
testKey `withDBs` fs
where
testKey f = withDB f (`SQL.exec` testSQL key) DBErrorOpen
+53 -28
View File
@@ -62,6 +62,7 @@ import Simplex.Chat.Remote.Types
import Simplex.Chat.Store (AutoAccept, StoreError (..), UserContactLink, UserMsgReceiptSettings)
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Util (liftIOEither)
import Simplex.FileTransfer.Description (FileDescriptionURI)
import Simplex.Messaging.Agent (AgentClient, SubscriptionsInfo)
import Simplex.Messaging.Agent.Client (AgentLocks, AgentWorkersDetails (..), AgentWorkersSummary (..), ProtocolTestFailure)
@@ -82,7 +83,7 @@ import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), Cor
import Simplex.Messaging.TMap (TMap)
import Simplex.Messaging.Transport (TLS, simplexMQVersion)
import Simplex.Messaging.Transport.Client (TransportHost)
import Simplex.Messaging.Util (allFinally, catchAllErrors, liftIOEither, tryAllErrors, (<$$>))
import Simplex.Messaging.Util (allFinally, catchAllErrors, catchAllErrors', tryAllErrors, tryAllErrors', (<$$>))
import Simplex.RemoteControl.Client
import Simplex.RemoteControl.Invitation (RCSignedInvitation, RCVerifiedInvitation)
import Simplex.RemoteControl.Types
@@ -1140,7 +1141,7 @@ data DatabaseError
data SQLiteError = SQLiteErrorNotADatabase | SQLiteError String
deriving (Show, Exception)
throwDBError :: ChatMonad m => DatabaseError -> m ()
throwDBError :: DatabaseError -> CM ()
throwDBError = throwError . ChatErrorDatabase
-- TODO review errors, some of it can be covered by HTTP2 errors
@@ -1244,39 +1245,59 @@ data RemoteCtrlInfo = RemoteCtrlInfo
}
deriving (Show)
type ChatMonad' m = (MonadUnliftIO m, MonadReader ChatController m)
type CM' a = ReaderT ChatController IO a
type ChatMonad m = (ChatMonad' m, MonadError ChatError m)
type CM a = ExceptT ChatError (ReaderT ChatController IO) a
chatReadVar :: ChatMonad' m => (ChatController -> TVar a) -> m a
chatReadVar f = asks f >>= readTVarIO
chatReadVar :: (ChatController -> TVar a) -> CM a
chatReadVar = lift . chatReadVar'
{-# INLINE chatReadVar #-}
chatWriteVar :: ChatMonad' m => (ChatController -> TVar a) -> a -> m ()
chatWriteVar f value = asks f >>= atomically . (`writeTVar` value)
chatReadVar' :: (ChatController -> TVar a) -> CM' a
chatReadVar' f = asks f >>= readTVarIO
{-# INLINE chatReadVar' #-}
chatWriteVar :: (ChatController -> TVar a) -> a -> CM ()
chatWriteVar f = lift . chatWriteVar' f
{-# INLINE chatWriteVar #-}
chatModifyVar :: ChatMonad' m => (ChatController -> TVar a) -> (a -> a) -> m ()
chatModifyVar f newValue = asks f >>= atomically . (`modifyTVar'` newValue)
chatWriteVar' :: (ChatController -> TVar a) -> a -> CM' ()
chatWriteVar' f value = asks f >>= atomically . (`writeTVar` value)
{-# INLINE chatWriteVar' #-}
chatModifyVar :: (ChatController -> TVar a) -> (a -> a) -> CM ()
chatModifyVar f = lift . chatModifyVar' f
{-# INLINE chatModifyVar #-}
setContactNetworkStatus :: ChatMonad' m => Contact -> NetworkStatus -> m ()
setContactNetworkStatus Contact {activeConn = Nothing} _ = pure ()
setContactNetworkStatus Contact {activeConn = Just Connection {agentConnId}} status = chatModifyVar connNetworkStatuses $ M.insert agentConnId status
chatModifyVar' :: (ChatController -> TVar a) -> (a -> a) -> CM' ()
chatModifyVar' f newValue = asks f >>= atomically . (`modifyTVar'` newValue)
{-# INLINE chatModifyVar' #-}
tryChatError :: ChatMonad m => m a -> m (Either ChatError a)
setContactNetworkStatus :: Contact -> NetworkStatus -> CM' ()
setContactNetworkStatus Contact {activeConn = Nothing} _ = pure ()
setContactNetworkStatus Contact {activeConn = Just Connection {agentConnId}} status = chatModifyVar' connNetworkStatuses $ M.insert agentConnId status
tryChatError :: CM a -> CM (Either ChatError a)
tryChatError = tryAllErrors mkChatError
{-# INLINE tryChatError #-}
catchChatError :: ChatMonad m => m a -> (ChatError -> m a) -> m a
tryChatError' :: CM a -> CM' (Either ChatError a)
tryChatError' = tryAllErrors' mkChatError
{-# INLINE tryChatError' #-}
catchChatError :: CM a -> (ChatError -> CM a) -> CM a
catchChatError = catchAllErrors mkChatError
{-# INLINE catchChatError #-}
chatFinally :: ChatMonad m => m a -> m b -> m a
catchChatError' :: CM a -> (ChatError -> CM' a) -> CM' a
catchChatError' = catchAllErrors' mkChatError
{-# INLINE catchChatError' #-}
chatFinally :: CM a -> CM b -> CM a
chatFinally = allFinally mkChatError
{-# INLINE chatFinally #-}
onChatError :: ChatMonad m => m a -> m b -> m a
onChatError :: CM a -> CM b -> CM a
a `onChatError` onErr = a `catchChatError` \e -> onErr >> throwError e
{-# INLINE onChatError #-}
@@ -1295,12 +1316,16 @@ mkStoreError = SEInternalError . show
chatCmdError :: Maybe User -> String -> ChatResponse
chatCmdError user = CRChatCmdError user . ChatError . CECommandError
throwChatError :: ChatMonad m => ChatErrorType -> m a
throwChatError :: ChatErrorType -> CM a
throwChatError = throwError . ChatError
-- | Emit local events.
toView :: ChatMonad' m => ChatResponse -> m ()
toView ev = do
toView :: ChatResponse -> CM ()
toView = lift . toView'
{-# INLINE toView #-}
toView' :: ChatResponse -> CM' ()
toView' ev = do
cc@ChatController {outputQ = localQ, remoteCtrlSession = session, config = ChatConfig {chatHooks}} <- ask
event <- liftIO $ eventHook chatHooks cc ev
atomically $
@@ -1310,15 +1335,15 @@ toView ev = do
-- TODO potentially, it should hold some events while connecting
_ -> writeTBQueue localQ (Nothing, Nothing, event)
withStore' :: ChatMonad m => (DB.Connection -> IO a) -> m a
withStore' :: (DB.Connection -> IO a) -> CM a
withStore' action = withStore $ liftIO . action
withStore :: ChatMonad m => (DB.Connection -> ExceptT StoreError IO a) -> m a
withStore :: (DB.Connection -> ExceptT StoreError IO a) -> CM a
withStore action = do
ChatController {chatStore} <- ask
liftIOEither $ withTransaction chatStore (runExceptT . withExceptT ChatErrorStore . action) `E.catches` handleDBErrors
withStoreBatch :: (ChatMonad' m, Traversable t) => (DB.Connection -> t (IO (Either ChatError a))) -> m (t (Either ChatError a))
withStoreBatch :: Traversable t => (DB.Connection -> t (IO (Either ChatError a))) -> CM' (t (Either ChatError a))
withStoreBatch actions = do
ChatController {chatStore} <- ask
liftIO $ withTransaction chatStore $ mapM (`E.catches` handleDBErrors) . actions
@@ -1332,17 +1357,17 @@ handleDBErrors =
E.Handler $ \(E.SomeException e) -> pure . Left . ChatErrorStore . SEDBException $ show e
]
withStoreBatch' :: (ChatMonad' m, Traversable t) => (DB.Connection -> t (IO a)) -> m (t (Either ChatError a))
withStoreBatch' :: Traversable t => (DB.Connection -> t (IO a)) -> CM' (t (Either ChatError a))
withStoreBatch' actions = withStoreBatch $ fmap (fmap Right) . actions
withAgent :: ChatMonad m => (AgentClient -> ExceptT AgentErrorType m a) -> m a
withAgent :: (AgentClient -> ExceptT AgentErrorType IO a) -> CM a
withAgent action =
asks smpAgent
>>= runExceptT . action
>>= liftIO . runExceptT . action
>>= liftEither . first (`ChatErrorAgent` Nothing)
withAgent' :: ChatMonad' m => (AgentClient -> m a) -> m a
withAgent' action = asks smpAgent >>= action
withAgent' :: (AgentClient -> IO a) -> CM' a
withAgent' action = asks smpAgent >>= liftIO . action
$(JQ.deriveJSON (enumJSON $ dropPrefix "HS") ''HelpSection)
+4 -5
View File
@@ -3,13 +3,12 @@
module Simplex.Chat.Files where
import Control.Monad.IO.Class
import Simplex.Chat.Controller
import Simplex.Messaging.Util (ifM)
import System.FilePath (combine, splitExtensions)
import UnliftIO.Directory (doesDirectoryExist, doesFileExist, getHomeDirectory, getTemporaryDirectory)
uniqueCombine :: MonadIO m => FilePath -> String -> m FilePath
uniqueCombine :: FilePath -> String -> IO FilePath
uniqueCombine fPath fName = tryCombine (0 :: Int)
where
tryCombine n =
@@ -18,10 +17,10 @@ uniqueCombine fPath fName = tryCombine (0 :: Int)
f = fPath `combine` (name <> suffix <> ext)
in ifM (doesFileExist f) (tryCombine $ n + 1) (pure f)
getChatTempDirectory :: ChatMonad m => m FilePath
getChatTempDirectory = chatReadVar tempDirectory >>= maybe getTemporaryDirectory pure
getChatTempDirectory :: CM' FilePath
getChatTempDirectory = chatReadVar' tempDirectory >>= maybe getTemporaryDirectory pure
getDefaultFilesFolder :: ChatMonad m => m FilePath
getDefaultFilesFolder :: CM' FilePath
getDefaultFilesFolder = do
dir <- (`combine` "Downloads") <$> getHomeDirectory
ifM (doesDirectoryExist dir) (pure dir) getChatTempDirectory
+1 -1
View File
@@ -24,6 +24,7 @@ import qualified Data.Aeson as J
import qualified Data.Aeson.Encoding as JE
import qualified Data.Aeson.TH as JQ
import qualified Data.Attoparsec.ByteString.Char8 as A
import qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Char (isSpace)
import Data.Int (Int64)
@@ -47,7 +48,6 @@ import Simplex.Chat.Types.Preferences
import Simplex.Messaging.Agent.Protocol (AgentMsgId, MsgMeta (..), MsgReceiptStatus (..))
import Simplex.Messaging.Crypto.File (CryptoFile (..))
import qualified Simplex.Messaging.Crypto.File as CF
import qualified Simplex.Messaging.Encoding.Base64 as B64
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_, parseAll, sumTypeJSON)
import Simplex.Messaging.Protocol (MsgBody)
+1 -1
View File
@@ -17,6 +17,7 @@ import qualified Data.Aeson.TH as JQ
import Data.Bifunctor (first)
import Data.ByteArray (ScrubbedBytes)
import qualified Data.ByteArray as BA
import qualified Data.ByteString.Base64.URL as U
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
@@ -49,7 +50,6 @@ import Simplex.Messaging.Agent.Env.SQLite (createAgentStore)
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, closeSQLiteStore, reopenSQLiteStore)
import Simplex.Messaging.Client (defaultNetworkConfig)
import qualified Simplex.Messaging.Crypto as C
import qualified Simplex.Messaging.Encoding.Base64.URL as U
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, sumTypeJSON)
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), BasicAuth (..), CorrId (..), ProtoServerWithAuth (..), ProtocolServer (..))
+1 -1
View File
@@ -17,6 +17,7 @@ import Data.Bifunctor (bimap)
import qualified Data.ByteArray as BA
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import qualified Data.ByteString.Base64.URL as U
import Data.Either (fromLeft)
import Data.Word (Word8)
import Foreign.C (CInt, CString, newCAString)
@@ -25,7 +26,6 @@ import Foreign.StablePtr
import Simplex.Chat.Controller (ChatController (..))
import Simplex.Chat.Mobile.Shared
import qualified Simplex.Messaging.Crypto as C
import qualified Simplex.Messaging.Encoding.Base64.URL as U
import UnliftIO (atomically)
cChatEncryptMedia :: StablePtr ChatController -> CString -> Ptr Word8 -> CInt -> IO CString
+86 -80
View File
@@ -22,6 +22,7 @@ import Crypto.Random (getRandomBytes)
import qualified Data.Aeson as J
import qualified Data.Aeson.Types as JT
import Data.ByteString (ByteString)
import qualified Data.ByteString.Base64.URL as B64U
import Data.ByteString.Builder (Builder)
import qualified Data.ByteString.Char8 as B
import Data.Functor (($>))
@@ -49,13 +50,12 @@ import Simplex.Chat.Store.Files
import Simplex.Chat.Store.Remote
import Simplex.Chat.Store.Shared
import Simplex.Chat.Types
import Simplex.Chat.Util (encryptFile)
import Simplex.Chat.Util (liftIOEither, encryptFile)
import Simplex.FileTransfer.Description (FileDigest (..))
import Simplex.Messaging.Agent
import Simplex.Messaging.Agent.Protocol (AgentErrorType (RCP))
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
import qualified Simplex.Messaging.Crypto.File as CF
import qualified Simplex.Messaging.Encoding.Base64.URL as B64U
import Simplex.Messaging.Encoding.String (StrEncoding (..))
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport (TLS, closeConnection, tlsUniq)
@@ -95,7 +95,7 @@ discoveryTimeout = 60000000
-- * Desktop side
getRemoteHostClient :: ChatMonad m => RemoteHostId -> m RemoteHostClient
getRemoteHostClient :: RemoteHostId -> CM RemoteHostClient
getRemoteHostClient rhId = do
sessions <- asks remoteHostSessions
liftIOEither . atomically $
@@ -106,7 +106,7 @@ getRemoteHostClient rhId = do
where
rhKey = RHId rhId
withRemoteHostSession :: ChatMonad m => RHKey -> SessionSeq -> (RemoteHostSession -> Either ChatError (a, RemoteHostSession)) -> m a
withRemoteHostSession :: RHKey -> SessionSeq -> (RemoteHostSession -> Either ChatError (a, RemoteHostSession)) -> CM a
withRemoteHostSession rhKey sseq f = do
sessions <- asks remoteHostSessions
r <-
@@ -121,7 +121,7 @@ withRemoteHostSession rhKey sseq f = do
liftEither r
-- | Transition session state with a 'RHNew' ID to an assigned 'RemoteHostId'
setNewRemoteHostId :: ChatMonad m => SessionSeq -> RemoteHostId -> m ()
setNewRemoteHostId :: SessionSeq -> RemoteHostId -> CM ()
setNewRemoteHostId sseq rhId = do
sessions <- asks remoteHostSessions
liftIOEither . atomically $ do
@@ -136,13 +136,13 @@ setNewRemoteHostId sseq rhId = do
where
err = pure . Left . ChatErrorRemoteHost RHNew
startRemoteHost :: ChatMonad m => Maybe (RemoteHostId, Bool) -> Maybe RCCtrlAddress -> Maybe Word16 -> m (NonEmpty RCCtrlAddress, Maybe RemoteHostInfo, RCSignedInvitation)
startRemoteHost :: Maybe (RemoteHostId, Bool) -> Maybe RCCtrlAddress -> Maybe Word16 -> CM (NonEmpty RCCtrlAddress, Maybe RemoteHostInfo, RCSignedInvitation)
startRemoteHost rh_ rcAddrPrefs_ port_ = do
(rhKey, multicast, remoteHost_, pairing) <- case rh_ of
Just (rhId, multicast) -> do
rh@RemoteHost {hostPairing} <- withStore $ \db -> getRemoteHost db rhId
pure (RHId rhId, multicast, Just $ remoteHostInfo rh $ Just RHSStarting, hostPairing) -- get from the database, start multicast if requested
Nothing -> withAgent $ \a -> (RHNew,False,Nothing,) <$> rcNewHostPairing a
Nothing -> lift . withAgent' $ \a -> (RHNew,False,Nothing,) <$> rcNewHostPairing a
sseq <- startRemoteHostSession rhKey
ctrlAppInfo <- mkCtrlAppInfo
(localAddrs, invitation, rchClient, vars) <- handleConnectError rhKey sseq . withAgent $ \a -> rcConnectHost a pairing (J.toJSON ctrlAppInfo) multicast rcAddrPrefs_ port_
@@ -170,18 +170,18 @@ startRemoteHost rh_ rcAddrPrefs_ port_ = do
unless (isAppCompatible appVersion ctrlAppVersionRange) $ throwError $ RHEBadVersion appVersion
when (encoding == PEKotlin && localEncoding == PESwift) $ throwError $ RHEProtocolError RPEIncompatibleEncoding
pure hostInfo
handleConnectError :: ChatMonad m => RHKey -> SessionSeq -> m a -> m a
handleConnectError :: RHKey -> SessionSeq -> CM a -> CM a
handleConnectError rhKey sessSeq action =
action `catchChatError` \err -> do
logError $ "startRemoteHost.rcConnectHost crashed: " <> tshow err
cancelRemoteHostSession (Just (sessSeq, RHSRConnectionFailed err)) rhKey
throwError err
handleHostError :: ChatMonad m => SessionSeq -> TVar RHKey -> m () -> m ()
handleHostError :: SessionSeq -> TVar RHKey -> CM () -> CM ()
handleHostError sessSeq rhKeyVar action =
action `catchChatError` \err -> do
logError $ "startRemoteHost.waitForHostSession crashed: " <> tshow err
readTVarIO rhKeyVar >>= cancelRemoteHostSession (Just (sessSeq, RHSRCrashed err))
waitForHostSession :: ChatMonad m => Maybe RemoteHostInfo -> RHKey -> SessionSeq -> Maybe RCCtrlAddress -> TVar RHKey -> RCStepTMVar (ByteString, TLS, RCStepTMVar (RCHostSession, RCHostHello, RCHostPairing)) -> m ()
waitForHostSession :: Maybe RemoteHostInfo -> RHKey -> SessionSeq -> Maybe RCCtrlAddress -> TVar RHKey -> RCStepTMVar (ByteString, TLS, RCStepTMVar (RCHostSession, RCHostHello, RCHostPairing)) -> CM ()
waitForHostSession remoteHost_ rhKey sseq rcAddr_ rhKeyVar vars = do
(sessId, tls, vars') <- timeoutThrow (ChatErrorRemoteHost rhKey RHETimeout) 60000000 $ takeRCStep vars
let sessionCode = verificationCode sessId
@@ -203,7 +203,7 @@ startRemoteHost rh_ rcAddrPrefs_ port_ = do
toView $ CRNewRemoteHost rhi
-- set up HTTP transport and remote profile protocol
disconnected <- toIO $ onDisconnected rhKey' sseq
httpClient <- liftEitherError (httpError remoteHostId) $ attachRevHTTP2Client disconnected tls
httpClient <- liftError' (httpError remoteHostId) $ attachRevHTTP2Client disconnected tls
rhClient <- mkRemoteHostClient httpClient sessionKeys sessId storePath hostInfo
pollAction <- async $ pollEvents remoteHostId rhClient
withRemoteHostSession rhKey' sseq $ \case
@@ -211,7 +211,7 @@ startRemoteHost rh_ rcAddrPrefs_ port_ = do
_ -> Left $ ChatErrorRemoteHost rhKey RHEBadState
chatWriteVar currentRemoteHost $ Just remoteHostId -- this is required for commands to be passed to remote host
toView $ CRRemoteHostConnected rhi {sessionState = Just RHSConnected {sessionCode}}
upsertRemoteHost :: ChatMonad m => RCHostPairing -> Maybe RemoteHostInfo -> Maybe RCCtrlAddress -> Text -> SessionSeq -> RemoteHostSessionState -> m RemoteHostInfo
upsertRemoteHost :: RCHostPairing -> Maybe RemoteHostInfo -> Maybe RCCtrlAddress -> Text -> SessionSeq -> RemoteHostSessionState -> CM RemoteHostInfo
upsertRemoteHost pairing'@RCHostPairing {knownHost = kh_} rhi_ rcAddr_ hostDeviceName sseq state = do
KnownHostPairing {hostDhPubKey = hostDhPubKey'} <- maybe (throwError . ChatError $ CEInternalError "KnownHost is known after verification") pure kh_
case rhi_ of
@@ -223,11 +223,11 @@ startRemoteHost rh_ rcAddrPrefs_ port_ = do
Just rhi@RemoteHostInfo {remoteHostId} -> do
withStore' $ \db -> updateHostPairing db remoteHostId hostDeviceName hostDhPubKey' rcAddr_ port_
pure (rhi :: RemoteHostInfo) {sessionState = Just state}
onDisconnected :: ChatMonad m => RHKey -> SessionSeq -> m ()
onDisconnected :: RHKey -> SessionSeq -> CM ()
onDisconnected rhKey sseq = do
logDebug $ "HTTP2 client disconnected: " <> tshow (rhKey, sseq)
cancelRemoteHostSession (Just (sseq, RHSRDisconnected)) rhKey
pollEvents :: ChatMonad m => RemoteHostId -> RemoteHostClient -> m ()
pollEvents :: RemoteHostId -> RemoteHostClient -> CM ()
pollEvents rhId rhClient = do
oq <- asks outputQ
forever $ do
@@ -236,7 +236,7 @@ startRemoteHost rh_ rcAddrPrefs_ port_ = do
httpError :: RemoteHostId -> HTTP2ClientError -> ChatError
httpError rhId = ChatErrorRemoteHost (RHId rhId) . RHEProtocolError . RPEHTTP2 . tshow
startRemoteHostSession :: ChatMonad m => RHKey -> m SessionSeq
startRemoteHostSession :: RHKey -> CM SessionSeq
startRemoteHostSession rhKey = do
sessions <- asks remoteHostSessions
nextSessionSeq <- asks remoteSessionSeq
@@ -247,12 +247,12 @@ startRemoteHostSession rhKey = do
sessionSeq <- stateTVar nextSessionSeq $ \s -> (s, s + 1)
Right sessionSeq <$ TM.insert rhKey (sessionSeq, RHSessionStarting) sessions
closeRemoteHost :: ChatMonad m => RHKey -> m ()
closeRemoteHost :: RHKey -> CM ()
closeRemoteHost rhKey = do
logNote $ "Closing remote host session for " <> tshow rhKey
cancelRemoteHostSession Nothing rhKey
cancelRemoteHostSession :: ChatMonad m => Maybe (SessionSeq, RemoteHostStopReason) -> RHKey -> m ()
cancelRemoteHostSession :: Maybe (SessionSeq, RemoteHostStopReason) -> RHKey -> CM ()
cancelRemoteHostSession handlerInfo_ rhKey = do
sessions <- asks remoteHostSessions
crh <- asks currentRemoteHost
@@ -299,7 +299,7 @@ cancelRemoteHost handlingError = \case
randomStorePath :: IO FilePath
randomStorePath = B.unpack . B64U.encode <$> getRandomBytes 12
listRemoteHosts :: ChatMonad m => m [RemoteHostInfo]
listRemoteHosts :: CM [RemoteHostInfo]
listRemoteHosts = do
sessions <- chatReadVar remoteHostSessions
map (rhInfo sessions) <$> withStore' getRemoteHosts
@@ -307,7 +307,7 @@ listRemoteHosts = do
rhInfo sessions rh@RemoteHost {remoteHostId} =
remoteHostInfo rh $ rhsSessionState . snd <$> M.lookup (RHId remoteHostId) sessions
switchRemoteHost :: ChatMonad m => Maybe RemoteHostId -> m (Maybe RemoteHostInfo)
switchRemoteHost :: Maybe RemoteHostId -> CM (Maybe RemoteHostInfo)
switchRemoteHost rhId_ = do
rhi_ <- forM rhId_ $ \rhId -> do
let rhKey = RHId rhId
@@ -322,7 +322,7 @@ remoteHostInfo :: RemoteHost -> Maybe RemoteHostSessionState -> RemoteHostInfo
remoteHostInfo RemoteHost {remoteHostId, storePath, hostDeviceName, bindAddress_, bindPort_} sessionState =
RemoteHostInfo {remoteHostId, storePath, hostDeviceName, bindAddress_, bindPort_, sessionState}
deleteRemoteHost :: ChatMonad m => RemoteHostId -> m ()
deleteRemoteHost :: RemoteHostId -> CM ()
deleteRemoteHost rhId = do
RemoteHost {storePath} <- withStore (`getRemoteHost` rhId)
chatReadVar remoteHostsFolder >>= \case
@@ -333,7 +333,7 @@ deleteRemoteHost rhId = do
Nothing -> logWarn "Local file store not available while deleting remote host"
withStore' (`deleteRemoteHostRecord` rhId)
storeRemoteFile :: forall m. ChatMonad m => RemoteHostId -> Maybe Bool -> FilePath -> m CryptoFile
storeRemoteFile :: RemoteHostId -> Maybe Bool -> FilePath -> CM CryptoFile
storeRemoteFile rhId encrypted_ localPath = do
c@RemoteHostClient {encryptHostFiles, storePath} <- getRemoteHostClient rhId
let encrypt = fromMaybe encryptHostFiles encrypted_
@@ -347,23 +347,23 @@ storeRemoteFile rhId encrypted_ localPath = do
(if encrypt then renameFile else copyFile) filePath hPath
pure (cf :: CryptoFile) {filePath = filePath'}
where
encryptLocalFile :: m CryptoFile
encryptLocalFile :: CM CryptoFile
encryptLocalFile = do
tmpDir <- getChatTempDirectory
tmpDir <- lift getChatTempDirectory
createDirectoryIfMissing True tmpDir
tmpFile <- tmpDir `uniqueCombine` takeFileName localPath
tmpFile <- liftIO $ tmpDir `uniqueCombine` takeFileName localPath
cfArgs <- atomically . CF.randomArgs =<< asks random
liftError (ChatError . CEFileWrite tmpFile) $ encryptFile localPath tmpFile cfArgs
pure $ CryptoFile tmpFile $ Just cfArgs
getRemoteFile :: ChatMonad m => RemoteHostId -> RemoteFile -> m ()
getRemoteFile :: RemoteHostId -> RemoteFile -> CM ()
getRemoteFile rhId rf = do
c@RemoteHostClient {storePath} <- getRemoteHostClient rhId
dir <- (</> storePath </> archiveFilesFolder) <$> (maybe getDefaultFilesFolder pure =<< chatReadVar remoteHostsFolder)
dir <- lift $ (</> storePath </> archiveFilesFolder) <$> (maybe getDefaultFilesFolder pure =<< chatReadVar' remoteHostsFolder)
createDirectoryIfMissing True dir
liftRH rhId $ remoteGetFile c dir rf
processRemoteCommand :: ChatMonad m => RemoteHostId -> RemoteHostClient -> ChatCommand -> ByteString -> m ChatResponse
processRemoteCommand :: RemoteHostId -> RemoteHostClient -> ChatCommand -> ByteString -> CM ChatResponse
processRemoteCommand remoteHostId c cmd s = case cmd of
SendFile chatName f -> sendFile "/f" chatName f
SendImage chatName f -> sendFile "/img" chatName f
@@ -378,7 +378,7 @@ processRemoteCommand remoteHostId c cmd s = case cmd of
maybe "" (\(CFArgs key nonce) -> "key=" <> strEncode key <> " nonce=" <> strEncode nonce <> " ") cryptoArgs
<> encodeUtf8 (T.pack filePath)
liftRH :: ChatMonad m => RemoteHostId -> ExceptT RemoteProtocolError IO a -> m a
liftRH :: RemoteHostId -> ExceptT RemoteProtocolError IO a -> CM a
liftRH rhId = liftError (ChatErrorRemoteHost (RHId rhId) . RHEProtocolError)
-- * Mobile side
@@ -386,7 +386,7 @@ liftRH rhId = liftError (ChatErrorRemoteHost (RHId rhId) . RHEProtocolError)
-- ** QR/link
-- | Use provided OOB link as an annouce
connectRemoteCtrlURI :: ChatMonad m => RCSignedInvitation -> m (Maybe RemoteCtrlInfo, CtrlAppInfo)
connectRemoteCtrlURI :: RCSignedInvitation -> CM (Maybe RemoteCtrlInfo, CtrlAppInfo)
connectRemoteCtrlURI signedInv = do
verifiedInv <- maybe (throwError $ ChatErrorRemoteCtrl RCEBadInvitation) pure $ verifySignedInvitation signedInv
sseq <- startRemoteCtrlSession
@@ -394,7 +394,7 @@ connectRemoteCtrlURI signedInv = do
-- ** Multicast
findKnownRemoteCtrl :: ChatMonad m => m ()
findKnownRemoteCtrl :: CM ()
findKnownRemoteCtrl = do
knownCtrls <- withStore' getRemoteCtrls
pairings <- case nonEmpty knownCtrls of
@@ -420,7 +420,7 @@ findKnownRemoteCtrl = do
_ -> Left $ ChatErrorRemoteCtrl RCEBadState
atomically $ putTMVar cmdOk ()
confirmRemoteCtrl :: ChatMonad m => RemoteCtrlId -> m (RemoteCtrlInfo, CtrlAppInfo)
confirmRemoteCtrl :: RemoteCtrlId -> CM (RemoteCtrlInfo, CtrlAppInfo)
confirmRemoteCtrl rcId = do
session <- asks remoteCtrlSession
(sseq, listener, found) <- liftIOEither $ atomically $ do
@@ -438,7 +438,7 @@ confirmRemoteCtrl rcId = do
-- ** Common
startRemoteCtrlSession :: ChatMonad m => m SessionSeq
startRemoteCtrlSession :: CM SessionSeq
startRemoteCtrlSession = do
session <- asks remoteCtrlSession
nextSessionSeq <- asks remoteSessionSeq
@@ -449,7 +449,7 @@ startRemoteCtrlSession = do
sseq <- stateTVar nextSessionSeq $ \s -> (s, s + 1)
Right sseq <$ writeTVar session (Just (sseq, RCSessionStarting))
connectRemoteCtrl :: ChatMonad m => RCVerifiedInvitation -> SessionSeq -> m (Maybe RemoteCtrlInfo, CtrlAppInfo)
connectRemoteCtrl :: RCVerifiedInvitation -> SessionSeq -> CM (Maybe RemoteCtrlInfo, CtrlAppInfo)
connectRemoteCtrl verifiedInv@(RCVerifiedInvitation inv@RCInvitation {ca, app}) sseq = handleCtrlError sseq RCSRConnectionFailed "connectRemoteCtrl" $ do
ctrlInfo@CtrlAppInfo {deviceName = ctrlDeviceName} <- parseCtrlAppInfo app
v <- checkAppVersion ctrlInfo
@@ -470,7 +470,7 @@ connectRemoteCtrl verifiedInv@(RCVerifiedInvitation inv@RCInvitation {ca, app})
where
validateRemoteCtrl RCInvitation {idkey} RemoteCtrl {ctrlPairing = RCCtrlPairing {idPubKey}} =
unless (idkey == idPubKey) $ throwError $ ChatErrorRemoteCtrl $ RCEProtocolError $ PRERemoteControl RCEIdentity
waitForCtrlSession :: ChatMonad m => Maybe RemoteCtrl -> Text -> RCCtrlClient -> RCStepTMVar (ByteString, TLS, RCStepTMVar (RCCtrlSession, RCCtrlPairing)) -> m ()
waitForCtrlSession :: Maybe RemoteCtrl -> Text -> RCCtrlClient -> RCStepTMVar (ByteString, TLS, RCStepTMVar (RCCtrlSession, RCCtrlPairing)) -> CM ()
waitForCtrlSession rc_ ctrlName rcsClient vars = do
(uniq, tls, rcsWaitConfirmation) <- timeoutThrow (ChatErrorRemoteCtrl RCETimeout) networkIOTimeout $ takeRCStep vars
let sessionCode = verificationCode uniq
@@ -489,18 +489,18 @@ connectRemoteCtrl verifiedInv@(RCVerifiedInvitation inv@RCInvitation {ca, app})
encryptFiles <- chatReadVar encryptLocalFiles
pure HostAppInfo {appVersion, deviceName = hostDeviceName, encoding = localEncoding, encryptFiles}
parseCtrlAppInfo :: ChatMonad m => JT.Value -> m CtrlAppInfo
parseCtrlAppInfo :: JT.Value -> CM CtrlAppInfo
parseCtrlAppInfo ctrlAppInfo = do
liftEitherWith (const $ ChatErrorRemoteCtrl RCEBadInvitation) $ JT.parseEither J.parseJSON ctrlAppInfo
handleRemoteCommand :: forall m. ChatMonad m => (ByteString -> m ChatResponse) -> RemoteCrypto -> TBQueue ChatResponse -> HTTP2Request -> m ()
handleRemoteCommand :: (ByteString -> CM' ChatResponse) -> RemoteCrypto -> TBQueue ChatResponse -> HTTP2Request -> CM' ()
handleRemoteCommand execChatCommand encryption remoteOutputQ HTTP2Request {request, reqBody, sendResponse} = do
logDebug "handleRemoteCommand"
liftRC (tryRemoteError parseRequest) >>= \case
liftIO (tryRemoteError' parseRequest) >>= \case
Right (getNext, rc) -> do
chatReadVar currentUser >>= \case
chatReadVar' currentUser >>= \case
Nothing -> replyError $ ChatError CENoActiveUser
Just user -> processCommand user getNext rc `catchChatError` replyError
Just user -> processCommand user getNext rc `catchChatError'` replyError
Left e -> reply $ RRProtocolError e
where
parseRequest :: ExceptT RemoteProtocolError IO (GetChunk, RemoteCommand)
@@ -508,67 +508,72 @@ handleRemoteCommand execChatCommand encryption remoteOutputQ HTTP2Request {reque
(header, getNext) <- parseDecryptHTTP2Body encryption request reqBody
(getNext,) <$> liftEitherWith RPEInvalidJSON (J.eitherDecode header)
replyError = reply . RRChatResponse . CRChatCmdError Nothing
processCommand :: User -> GetChunk -> RemoteCommand -> m ()
processCommand :: User -> GetChunk -> RemoteCommand -> CM ()
processCommand user getNext = \case
RCSend {command} -> handleSend execChatCommand command >>= reply
RCRecv {wait = time} -> handleRecv time remoteOutputQ >>= reply
RCStoreFile {fileName, fileSize, fileDigest} -> handleStoreFile encryption fileName fileSize fileDigest getNext >>= reply
RCSend {command} -> lift $ handleSend execChatCommand command >>= reply
RCRecv {wait = time} -> lift $ liftIO (handleRecv time remoteOutputQ) >>= reply
RCStoreFile {fileName, fileSize, fileDigest} -> lift $ handleStoreFile encryption fileName fileSize fileDigest getNext >>= reply
RCGetFile {file} -> handleGetFile encryption user file replyWith
reply :: RemoteResponse -> m ()
reply :: RemoteResponse -> CM' ()
reply = (`replyWith` \_ -> pure ())
replyWith :: Respond m
replyWith rr attach = do
resp <- liftRC $ encryptEncodeHTTP2Body encryption $ J.encode rr
liftIO . sendResponse . responseStreaming N.status200 [] $ \send flush -> do
send resp
attach send
flush
replyWith :: Respond
replyWith rr attach =
liftIO (tryRemoteError' . encryptEncodeHTTP2Body encryption $ J.encode rr) >>= \case
Right resp -> liftIO . sendResponse . responseStreaming N.status200 [] $ \send flush -> do
send resp
attach send
flush
Left e -> toView' . CRChatError Nothing . ChatErrorRemoteCtrl $ RCEProtocolError e
takeRCStep :: ChatMonad m => RCStepTMVar a -> m a
takeRCStep = liftEitherError (\e -> ChatErrorAgent {agentError = RCP e, connectionEntity_ = Nothing}) . atomically . takeTMVar
takeRCStep :: RCStepTMVar a -> CM a
takeRCStep = liftError' (\e -> ChatErrorAgent {agentError = RCP e, connectionEntity_ = Nothing}) . atomically . takeTMVar
type GetChunk = Int -> IO ByteString
type SendChunk = Builder -> IO ()
type Respond m = RemoteResponse -> (SendChunk -> IO ()) -> m ()
type Respond = RemoteResponse -> (SendChunk -> IO ()) -> CM' ()
liftRC :: ChatMonad m => ExceptT RemoteProtocolError IO a -> m a
liftRC :: ExceptT RemoteProtocolError IO a -> CM a
liftRC = liftError (ChatErrorRemoteCtrl . RCEProtocolError)
tryRemoteError :: ExceptT RemoteProtocolError IO a -> ExceptT RemoteProtocolError IO (Either RemoteProtocolError a)
tryRemoteError = tryAllErrors (RPEException . tshow)
{-# INLINE tryRemoteError #-}
handleSend :: ChatMonad m => (ByteString -> m ChatResponse) -> Text -> m RemoteResponse
tryRemoteError' :: ExceptT RemoteProtocolError IO a -> IO (Either RemoteProtocolError a)
tryRemoteError' = tryAllErrors' (RPEException . tshow)
{-# INLINE tryRemoteError' #-}
handleSend :: (ByteString -> CM' ChatResponse) -> Text -> CM' RemoteResponse
handleSend execChatCommand command = do
logDebug $ "Send: " <> tshow command
-- execChatCommand checks for remote-allowed commands
-- convert errors thrown in ChatMonad into error responses to prevent aborting the protocol wrapper
RRChatResponse <$> execChatCommand (encodeUtf8 command) `catchError` (pure . CRChatError Nothing)
-- convert errors thrown in execChatCommand into error responses to prevent aborting the protocol wrapper
RRChatResponse <$> execChatCommand (encodeUtf8 command)
handleRecv :: MonadUnliftIO m => Int -> TBQueue ChatResponse -> m RemoteResponse
handleRecv :: Int -> TBQueue ChatResponse -> IO RemoteResponse
handleRecv time events = do
logDebug $ "Recv: " <> tshow time
RRChatEvent <$> (timeout time . atomically $ readTBQueue events)
-- TODO this command could remember stored files and return IDs to allow removing files that are not needed.
-- Also, there should be some process removing unused files uploaded to remote host (possibly, all unused files).
handleStoreFile :: forall m. ChatMonad m => RemoteCrypto -> FilePath -> Word32 -> FileDigest -> GetChunk -> m RemoteResponse
handleStoreFile :: RemoteCrypto -> FilePath -> Word32 -> FileDigest -> GetChunk -> CM' RemoteResponse
handleStoreFile encryption fileName fileSize fileDigest getChunk =
either RRProtocolError RRFileStored <$> (chatReadVar filesFolder >>= storeFile)
either RRProtocolError RRFileStored <$> (chatReadVar' filesFolder >>= storeFile)
where
storeFile :: Maybe FilePath -> m (Either RemoteProtocolError FilePath)
storeFile :: Maybe FilePath -> CM' (Either RemoteProtocolError FilePath)
storeFile = \case
Just ff -> takeFileName <$$> storeFileTo ff
Nothing -> storeFileTo =<< getDefaultFilesFolder
storeFileTo :: FilePath -> m (Either RemoteProtocolError FilePath)
storeFileTo dir = liftRC . tryRemoteError $ do
filePath <- dir `uniqueCombine` fileName
storeFileTo :: FilePath -> CM' (Either RemoteProtocolError FilePath)
storeFileTo dir = liftIO . tryRemoteError' $ do
filePath <- liftIO $ dir `uniqueCombine` fileName
receiveEncryptedFile encryption getChunk fileSize fileDigest filePath
pure filePath
handleGetFile :: ChatMonad m => RemoteCrypto -> User -> RemoteFile -> Respond m -> m ()
handleGetFile :: RemoteCrypto -> User -> RemoteFile -> Respond -> CM ()
handleGetFile encryption User {userId} RemoteFile {userId = commandUserId, fileId, sent, fileSource = cf'@CryptoFile {filePath}} reply = do
logDebug $ "GetFile: " <> tshow filePath
unless (userId == commandUserId) $ throwChatError $ CEDifferentActiveUser {commandUserId, activeUserId = userId}
@@ -577,13 +582,13 @@ handleGetFile encryption User {userId} RemoteFile {userId = commandUserId, fileI
cf <- getLocalCryptoFile db commandUserId fileId sent
unless (cf == cf') $ throwError $ SEFileNotFound fileId
liftRC (tryRemoteError $ getFileInfo path) >>= \case
Left e -> reply (RRProtocolError e) $ \_ -> pure ()
Left e -> lift $ reply (RRProtocolError e) $ \_ -> pure ()
Right (fileSize, fileDigest) ->
withFile path ReadMode $ \h -> do
ExceptT . withFile path ReadMode $ \h -> runExceptT $ do
encFile <- liftRC $ prepareEncryptedFile encryption (h, fileSize)
reply RRFile {fileSize, fileDigest} $ sendEncryptedFile encFile
lift $ reply RRFile {fileSize, fileDigest} $ sendEncryptedFile encFile
listRemoteCtrls :: ChatMonad m => m [RemoteCtrlInfo]
listRemoteCtrls :: CM [RemoteCtrlInfo]
listRemoteCtrls = do
session <- snd <$$> chatReadVar remoteCtrlSession
let rcId = sessionRcId =<< session
@@ -604,7 +609,7 @@ remoteCtrlInfo RemoteCtrl {remoteCtrlId, ctrlDeviceName} sessionState =
RemoteCtrlInfo {remoteCtrlId, ctrlDeviceName, sessionState}
-- | Take a look at emoji of tlsunique, commit pairing, and start session server
verifyRemoteCtrlSession :: ChatMonad m => (ByteString -> m ChatResponse) -> Text -> m RemoteCtrlInfo
verifyRemoteCtrlSession :: (ByteString -> CM' ChatResponse) -> Text -> CM RemoteCtrlInfo
verifyRemoteCtrlSession execChatCommand sessCode' = do
(sseq, client, ctrlName, sessionCode, vars) <-
chatReadVar remoteCtrlSession >>= \case
@@ -619,14 +624,15 @@ verifyRemoteCtrlSession execChatCommand sessCode' = do
rc@RemoteCtrl {remoteCtrlId} <- upsertRemoteCtrl ctrlName rcCtrlPairing
remoteOutputQ <- asks (tbqSize . config) >>= newTBQueueIO
encryption <- mkCtrlRemoteCrypto sessionKeys $ tlsUniq tls
http2Server <- async $ attachHTTP2Server tls $ handleRemoteCommand execChatCommand encryption remoteOutputQ
cc <- ask
http2Server <- liftIO . async $ attachHTTP2Server tls $ \req -> handleRemoteCommand execChatCommand encryption remoteOutputQ req `runReaderT` cc
void . forkIO $ monitor sseq http2Server
updateRemoteCtrlSession sseq $ \case
RCSessionPendingConfirmation {} -> Right RCSessionConnected {remoteCtrlId, rcsClient = client, rcsSession, tls, http2Server, remoteOutputQ}
_ -> Left $ ChatErrorRemoteCtrl RCEBadState
pure $ remoteCtrlInfo rc $ Just RCSConnected {sessionCode = tlsSessionCode tls}
where
upsertRemoteCtrl :: ChatMonad m => Text -> RCCtrlPairing -> m RemoteCtrl
upsertRemoteCtrl :: Text -> RCCtrlPairing -> CM RemoteCtrl
upsertRemoteCtrl ctrlName rcCtrlPairing = withStore $ \db -> do
rc_ <- liftIO $ getRemoteCtrlByFingerprint db (ctrlFingerprint rcCtrlPairing)
case rc_ of
@@ -635,16 +641,16 @@ verifyRemoteCtrlSession execChatCommand sessCode' = do
let dhPrivKey' = dhPrivKey rcCtrlPairing
liftIO $ updateRemoteCtrl db rc ctrlName dhPrivKey'
pure rc {ctrlDeviceName = ctrlName, ctrlPairing = ctrlPairing {dhPrivKey = dhPrivKey'}}
monitor :: ChatMonad m => SessionSeq -> Async () -> m ()
monitor :: SessionSeq -> Async () -> CM ()
monitor sseq server = do
res <- waitCatch server
logInfo $ "HTTP2 server stopped: " <> tshow res
cancelActiveRemoteCtrl $ Just (sseq, RCSRDisconnected)
stopRemoteCtrl :: ChatMonad m => m ()
stopRemoteCtrl :: CM ()
stopRemoteCtrl = cancelActiveRemoteCtrl Nothing
handleCtrlError :: ChatMonad m => SessionSeq -> (ChatError -> RemoteCtrlStopReason) -> Text -> m a -> m a
handleCtrlError :: SessionSeq -> (ChatError -> RemoteCtrlStopReason) -> Text -> CM a -> CM a
handleCtrlError sseq mkReason name action =
action `catchChatError` \e -> do
logError $ name <> " remote ctrl error: " <> tshow e
@@ -652,7 +658,7 @@ handleCtrlError sseq mkReason name action =
throwError e
-- | Stop session controller, unless session update key is present but stale
cancelActiveRemoteCtrl :: ChatMonad m => Maybe (SessionSeq, RemoteCtrlStopReason) -> m ()
cancelActiveRemoteCtrl :: Maybe (SessionSeq, RemoteCtrlStopReason) -> CM ()
cancelActiveRemoteCtrl handlerInfo_ = handleAny (logError . tshow) $ do
var <- asks remoteCtrlSession
session_ <-
@@ -685,18 +691,18 @@ cancelRemoteCtrl handlingError = \case
cancelCtrlClient rcsClient
closeConnection tls
deleteRemoteCtrl :: ChatMonad m => RemoteCtrlId -> m ()
deleteRemoteCtrl :: RemoteCtrlId -> CM ()
deleteRemoteCtrl rcId = do
checkNoRemoteCtrlSession
-- TODO check it exists
withStore' (`deleteRemoteCtrlRecord` rcId)
checkNoRemoteCtrlSession :: ChatMonad m => m ()
checkNoRemoteCtrlSession :: CM ()
checkNoRemoteCtrlSession =
chatReadVar remoteCtrlSession >>= maybe (pure ()) (\_ -> throwError $ ChatErrorRemoteCtrl RCEBusy)
-- | Transition controller to a new state, unless session update key is stale
updateRemoteCtrlSession :: ChatMonad m => SessionSeq -> (RemoteCtrlSession -> Either ChatError RemoteCtrlSession) -> m ()
updateRemoteCtrlSession :: SessionSeq -> (RemoteCtrlSession -> Either ChatError RemoteCtrlSession) -> CM ()
updateRemoteCtrlSession sseq state = do
session <- asks remoteCtrlSession
r <- atomically $ do
+7 -7
View File
@@ -46,7 +46,7 @@ import Simplex.Messaging.Parsers (dropPrefix, taggedObjectJSON, pattern SingleFi
import Simplex.Messaging.Transport.Buffer (getBuffered)
import Simplex.Messaging.Transport.HTTP2 (HTTP2Body (..), HTTP2BodyChunk, getBodyChunk)
import Simplex.Messaging.Transport.HTTP2.Client (HTTP2Client, HTTP2Response (..), closeHTTP2Client, sendRequestDirect)
import Simplex.Messaging.Util (liftEitherError, liftEitherWith, liftError, tshow)
import Simplex.Messaging.Util (liftError', liftEitherWith, liftError, tshow)
import Simplex.RemoteControl.Client (xrcpBlockSize)
import qualified Simplex.RemoteControl.Client as RC
import Simplex.RemoteControl.Types (CtrlSessKeys (..), HostSessKeys (..), RCErrorType (..), SessionCode)
@@ -75,7 +75,7 @@ $(deriveJSON (taggedObjectJSON $ dropPrefix "RR") ''RemoteResponse)
-- * Client side / desktop
mkRemoteHostClient :: ChatMonad m => HTTP2Client -> HostSessKeys -> SessionCode -> FilePath -> HostAppInfo -> m RemoteHostClient
mkRemoteHostClient :: HTTP2Client -> HostSessKeys -> SessionCode -> FilePath -> HostAppInfo -> CM RemoteHostClient
mkRemoteHostClient httpClient sessionKeys sessionCode storePath HostAppInfo {encoding, deviceName, encryptFiles} = do
drg <- asks random
counter <- newTVarIO 1
@@ -92,15 +92,15 @@ mkRemoteHostClient httpClient sessionKeys sessionCode storePath HostAppInfo {enc
storePath
}
mkCtrlRemoteCrypto :: ChatMonad m => CtrlSessKeys -> SessionCode -> m RemoteCrypto
mkCtrlRemoteCrypto :: CtrlSessKeys -> SessionCode -> CM RemoteCrypto
mkCtrlRemoteCrypto CtrlSessKeys {hybridKey, idPubKey, sessPubKey} sessionCode = do
drg <- asks random
counter <- newTVarIO 1
let signatures = RSVerify {idPubKey, sessPubKey}
pure RemoteCrypto {drg, counter, sessionCode, hybridKey, signatures}
closeRemoteHostClient :: MonadIO m => RemoteHostClient -> m ()
closeRemoteHostClient RemoteHostClient {httpClient} = liftIO $ closeHTTP2Client httpClient
closeRemoteHostClient :: RemoteHostClient -> IO ()
closeRemoteHostClient RemoteHostClient {httpClient} = closeHTTP2Client httpClient
-- ** Commands
@@ -141,7 +141,7 @@ sendRemoteCommand :: RemoteHostClient -> Maybe (Handle, Word32) -> RemoteCommand
sendRemoteCommand RemoteHostClient {httpClient, hostEncoding, encryption} file_ cmd = do
encFile_ <- mapM (prepareEncryptedFile encryption) file_
req <- httpRequest encFile_ <$> encryptEncodeHTTP2Body encryption (J.encode cmd)
HTTP2Response {response, respBody} <- liftEitherError (RPEHTTP2 . tshow) $ sendRequestDirect httpClient req Nothing
HTTP2Response {response, respBody} <- liftError' (RPEHTTP2 . tshow) $ sendRequestDirect httpClient req Nothing
(header, getNext) <- parseDecryptHTTP2Body encryption response respBody
rr <- liftEitherWith (RPEInvalidJSON . fromString) $ J.eitherDecode header >>= JT.parseEither J.parseJSON . convertJSON hostEncoding localEncoding
pure (getNext, rr)
@@ -271,7 +271,7 @@ parseDecryptHTTP2Body RemoteCrypto {hybridKey, sessionCode, signatures} hr HTTP2
where
getSig = do
len <- liftIO $ B.head <$> getNext 1
liftEitherError RPEInvalidBody $ C.decodeSignature <$> getNext (fromIntegral len)
liftError' RPEInvalidBody $ C.decodeSignature <$> getNext (fromIntegral len)
verifySig key sig hc' = do
let signed = BA.convert $ CH.hashFinalize hc'
unless (C.verify' key sig signed) $ throwError $ PRERemoteControl RCECtrlAuth
+5 -7
View File
@@ -13,19 +13,17 @@ import Simplex.Messaging.Transport.HTTP2 (defaultHTTP2BufferSize, getHTTP2Body)
import Simplex.Messaging.Transport.HTTP2.Client (HTTP2Client, HTTP2ClientError (..), attachHTTP2Client, bodyHeadSize, connTimeout, defaultHTTP2ClientConfig)
import Simplex.Messaging.Transport.HTTP2.Server (HTTP2Request (..), runHTTP2ServerWith)
import Simplex.RemoteControl.Discovery
import UnliftIO
attachRevHTTP2Client :: IO () -> TLS -> IO (Either HTTP2ClientError HTTP2Client)
attachRevHTTP2Client disconnected = attachHTTP2Client config ANY_ADDR_V4 "0" disconnected defaultHTTP2BufferSize
where
config = defaultHTTP2ClientConfig {bodyHeadSize = doNotPrefetchHead, connTimeout = maxBound}
attachHTTP2Server :: MonadUnliftIO m => TLS -> (HTTP2Request -> m ()) -> m ()
attachHTTP2Server tls processRequest = do
withRunInIO $ \unlift ->
runHTTP2ServerWith defaultHTTP2BufferSize ($ tls) $ \sessionId r sendResponse -> do
reqBody <- getHTTP2Body r doNotPrefetchHead
unlift $ processRequest HTTP2Request {sessionId, request = r, reqBody, sendResponse}
attachHTTP2Server :: TLS -> (HTTP2Request -> IO ()) -> IO ()
attachHTTP2Server tls processRequest =
runHTTP2ServerWith defaultHTTP2BufferSize ($ tls) $ \sessionId r sendResponse -> do
reqBody <- getHTTP2Body r doNotPrefetchHead
processRequest HTTP2Request {sessionId, request = r, reqBody, sendResponse}
-- | Suppress storing initial chunk in bodyHead, forcing clients and servers to stream chunks
doNotPrefetchHead :: Int
+4 -4
View File
@@ -15,7 +15,7 @@ import Simplex.FileTransfer.Transport (ReceiveFileError (..), receiveSbFile, sen
import qualified Simplex.Messaging.Crypto as C
import qualified Simplex.Messaging.Crypto.Lazy as LC
import Simplex.Messaging.Encoding
import Simplex.Messaging.Util (liftEitherError, liftEitherWith)
import Simplex.Messaging.Util (liftError', liftEitherWith)
import Simplex.RemoteControl.Types (RCErrorType (..))
import UnliftIO
import UnliftIO.Directory (getFileSize)
@@ -37,11 +37,11 @@ receiveEncryptedFile :: RemoteCrypto -> (Int -> IO ByteString) -> Word32 -> File
receiveEncryptedFile RemoteCrypto {hybridKey} getChunk fileSize fileDigest toPath = do
c <- liftIO $ getChunk 1
unless (c == "\x01") $ throwError RPENoFile
nonce <- liftEitherError RPEInvalidBody $ smpDecode <$> getChunk 24
size <- liftEitherError RPEInvalidBody $ smpDecode <$> getChunk 4
nonce <- liftError' RPEInvalidBody $ smpDecode <$> getChunk 24
size <- liftError' RPEInvalidBody $ smpDecode <$> getChunk 4
unless (size == fileSize + fromIntegral C.authTagSize) $ throwError RPEFileSize
sbState <- liftEitherWith (const $ PRERemoteControl RCEDecrypt) $ LC.kcbInit hybridKey nonce
liftEitherError fErr $ withFile toPath WriteMode $ \h -> receiveSbFile getChunk h sbState fileSize
liftError' fErr $ withFile toPath WriteMode $ \h -> receiveSbFile getChunk h sbState fileSize
digest <- liftIO $ LC.sha512Hash <$> LB.readFile toPath
unless (FileDigest digest == fileDigest) $ throwError RPEFileDigest
where
+1 -1
View File
@@ -18,6 +18,7 @@ import Control.Monad.Except
import Control.Monad.IO.Class
import Crypto.Random (ChaChaDRG)
import qualified Data.Aeson.TH as J
import qualified Data.ByteString.Base64 as B64
import Data.ByteString.Char8 (ByteString)
import Data.Int (Int64)
import Data.Maybe (fromMaybe, isJust, listToMaybe)
@@ -38,7 +39,6 @@ import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..))
import qualified Simplex.Messaging.Crypto.Ratchet as CR
import qualified Simplex.Messaging.Encoding.Base64 as B64
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
import Simplex.Messaging.Protocol (SubscriptionMode (..))
import Simplex.Messaging.Util (allFinally)
+5 -1
View File
@@ -1,6 +1,6 @@
{-# LANGUAGE TupleSections #-}
module Simplex.Chat.Util (week, encryptFile, chunkSize, shuffle) where
module Simplex.Chat.Util (week, encryptFile, chunkSize, liftIOEither, shuffle) where
import Control.Monad
import Control.Monad.Except
@@ -42,3 +42,7 @@ shuffle xs = map snd . sortBy (comparing fst) <$> mapM (\x -> (,x) <$> random) x
where
random :: IO Word16
random = randomRIO (0, 65535)
liftIOEither :: (MonadIO m, MonadError e m) => IO (Either e a) -> m a
liftIOEither a = liftIO a >>= liftEither
{-# INLINE liftIOEither #-}
+1 -1
View File
@@ -87,7 +87,7 @@ testOpts =
testCoreOpts :: CoreChatOpts
testCoreOpts =
CoreChatOpts
{ dbFilePrefix = undefined,
{ dbFilePrefix = "./simplex_v1",
dbKey = "",
-- dbKey = "this is a pass-phrase to encrypt the database",
smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001"],
+1 -1
View File
@@ -14,6 +14,7 @@ import Control.Concurrent.STM
import Control.Monad (unless, when)
import Control.Monad.Except (runExceptT)
import Data.ByteString (ByteString)
import qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString.Char8 as B
import Data.Char (isDigit)
import Data.List (isPrefixOf, isSuffixOf)
@@ -34,7 +35,6 @@ import Simplex.Messaging.Agent.Store.SQLite (maybeFirstRow, withTransaction)
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport, pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff)
import qualified Simplex.Messaging.Encoding.Base64 as B64
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Version
import System.Directory (doesFileExist)
+3 -3
View File
@@ -4,12 +4,12 @@ module WebRTCTests where
import Control.Monad.Except
import Crypto.Random (getRandomBytes)
import qualified Data.ByteString.Base64.URL as U
import qualified Data.ByteString.Char8 as B
import Foreign.StablePtr
import Simplex.Chat.Mobile
import Simplex.Chat.Mobile.WebRTC
import qualified Simplex.Messaging.Crypto as C
import qualified Simplex.Messaging.Encoding.Base64.URL as U
import System.FilePath ((</>))
import Test.Hspec
@@ -36,8 +36,8 @@ webRTCTests = describe "WebRTC crypto" $ do
cc <- newStablePtr c
let key = B.replicate 32 '#'
frame <- (<> B.replicate reservedSize '\NUL') <$> getRandomBytes 100
runExceptT (chatEncryptMedia cc key frame) `shouldReturn` Left "invalid key: invalid base64 encoding near offset: 0"
runExceptT (chatDecryptMedia key frame) `shouldReturn` Left "invalid key: invalid base64 encoding near offset: 0"
runExceptT (chatEncryptMedia cc key frame) `shouldReturn` Left "invalid key: invalid character at offset: 0"
runExceptT (chatDecryptMedia key frame) `shouldReturn` Left "invalid key: invalid character at offset: 0"
it "should fail on invalid auth tag" $ \tmp -> do
Right c <- chatMigrateInit (tmp </> "1") "" "yesUp"
cc <- newStablePtr c