Compare commits

..

25 Commits

Author SHA1 Message Date
Evgeny Poberezkin 22c62d544c Merge branch 'ep/remove-mtl' into ab/ios-haskell-init 2024-03-31 12:26:52 +01:00
Evgeny Poberezkin fbe9a4da39 strict data, optimization 2024-03-31 11:54:58 +01:00
Alexander Bondarenko 2995a79041 update to deMTLd branches 2024-03-31 13:48:52 +03:00
Alexander Bondarenko da197a35a0 Merge remote-tracking branch 'origin/ep/remove-mtl' into ab/ios-haskell-init 2024-03-31 11:27:14 +03:00
Evgeny Poberezkin 6f8578d0f1 core: remove mtl typeclasses to reduce overhead 2024-03-30 23:49:50 +00:00
Alexander Bondarenko 73cabd3c69 add info tables 2024-03-30 16:35:46 +03:00
Alexander Bondarenko c30c181209 unify init_haskell to allow nse profiling 2024-03-30 15:47:48 +03:00
Alexander Bondarenko 701b4186b6 Merge remote-tracking branch 'origin/master' into ios-profiling 2024-03-30 15:47:29 +03:00
Evgeny Poberezkin 9b6ca23dcb ios: fix UI freezes on start and when exiting background (#3972)
* ios: fix UI freezes on start and when exiting background

* remove unused code

* comments
2024-03-29 22:58:36 +00:00
Evgeny Poberezkin 257e03f10a Merge branch 'master' into ios-profiling 2024-03-29 19:44:17 +00:00
Evgeny Poberezkin 1410b4bd85 ios: prevent call to apiContactInfo after the app is suspended 2024-03-29 19:43:16 +00:00
Evgeny Poberezkin 42a35e8c72 ios: prevent call to listUsers after the app is suspended and database is closed 2024-03-29 18:58:55 +00:00
Evgeny Poberezkin 86fe28f1ed core: chat hooks allowing to extend or customize chat core (#3953)
* core: chat hooks allowing to extend or customize chat core

* update

* json

* custom response

* user in db queries
2024-03-29 18:30:17 +00:00
Evgeny Poberezkin 735359c279 android: strings for permissions 2024-03-29 18:28:54 +00:00
Stanislav Dmitrenko 3d20465662 android: more user-friendly way of granting permissions in call (#3945)
* android: more user-friendly way of granting permissions in call

* changes

* ask permissions without click at first

* changes in logic and UI

* enhancements

* show alert only when non in PiP

* change

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-03-29 18:19:36 +00:00
Alexander Bondarenko 7b644c0dcf Merge remote-tracking branch 'origin/master' into ios-profiling 2024-03-28 20:10:35 +02:00
Alexander Bondarenko c3d9d9a7c3 Merge remote-tracking branch 'origin/master' into ios-profiling 2024-03-27 17:40:14 +02:00
Alexander Bondarenko 261767035e Merge remote-tracking branch 'origin/master' into ios-profiling 2024-03-20 11:22:48 +02:00
spaced4ndy c9b00b3054 Merge branch 'master' into ios-profiling 2024-03-13 11:50:17 +04:00
spaced4ndy 1697190189 remove print 2024-03-12 20:52:05 +04:00
spaced4ndy 24609a98c6 fix paths 2024-03-12 20:49:42 +04:00
Alexander Bondarenko 60752feb9c fix addr 2024-03-12 17:09:54 +02:00
Alexander Bondarenko ff5ef638cd add missing include 2024-03-12 17:09:54 +02:00
spaced4ndy df619d540b ios: save debug profile, event log buttons (#3899) 2024-03-12 19:06:25 +04:00
Alexander Bondarenko 53d8a85b8c add RTS tuning to haskell_init 2024-03-12 16:58:44 +02:00
47 changed files with 1282 additions and 886 deletions
+20 -42
View File
@@ -21,6 +21,8 @@ public let CURRENT_CHAT_VERSION: Int = 2
// version range that supports establishing direct connection with a group member (xGrpDirectInvVRange in core)
public let CREATE_MEMBER_CONTACT_VRANGE = VersionRange(minVersion: 2, maxVersion: CURRENT_CHAT_VERSION)
private let networkStatusesLock = DispatchQueue(label: "chat.simplex.app.network-statuses.lock")
enum TerminalItem: Identifiable {
case cmd(Date, ChatCommand)
case resp(Date, ChatResponse)
@@ -1566,34 +1568,30 @@ func processReceivedMsg(_ res: ChatResponse) async {
m.removeChat(mergedContact.id)
}
}
case let .contactsSubscribed(_, contactRefs):
await updateContactsStatus(contactRefs, status: .connected)
case let .contactsDisconnected(_, contactRefs):
await updateContactsStatus(contactRefs, status: .disconnected)
case let .contactSubSummary(_, contactSubscriptions):
await MainActor.run {
for sub in contactSubscriptions {
// no need to update contact here, and it is slow
// if active(user) {
// m.updateContact(sub.contact)
// }
if let err = sub.contactError {
processContactSubError(sub.contact, err)
} else {
m.setContactNetworkStatus(sub.contact, .connected)
}
}
}
case let .networkStatus(status, connections):
await MainActor.run {
// dispatch queue to synchronize access
networkStatusesLock.sync {
var ns = m.networkStatuses
// slow loop is on the background thread
for cId in connections {
m.networkStatuses[cId] = status
ns[cId] = status
}
// fast model update is on the main thread
DispatchQueue.main.sync {
m.networkStatuses = ns
}
}
case let .networkStatuses(_, statuses): ()
await MainActor.run {
// dispatch queue to synchronize access
networkStatusesLock.sync {
var ns = m.networkStatuses
// slow loop is on the background thread
for s in statuses {
m.networkStatuses[s.agentConnId] = s.networkStatus
ns[s.agentConnId] = s.networkStatus
}
// fast model update is on the main thread
DispatchQueue.main.sync {
m.networkStatuses = ns
}
}
case let .newChatItem(user, aChatItem):
@@ -1944,26 +1942,6 @@ func chatItemSimpleUpdate(_ user: any UserLike, _ aChatItem: AChatItem) async {
}
}
func updateContactsStatus(_ contactRefs: [ContactRef], status: NetworkStatus) async {
let m = ChatModel.shared
await MainActor.run {
for c in contactRefs {
m.networkStatuses[c.agentConnId] = status
}
}
}
func processContactSubError(_ contact: Contact, _ chatError: ChatError) {
let m = ChatModel.shared
var err: String
switch chatError {
case .errorAgent(agentError: .BROKER(_, .NETWORK)): err = "network"
case .errorAgent(agentError: .SMP(smpErr: .AUTH)): err = "contact deleted"
default: err = String(describing: chatError)
}
m.setContactNetworkStatus(contact, .error(connectionError: err))
}
func refreshCallInvitations() throws {
let m = ChatModel.shared
let callInvitations = try justRefreshCallInvitations()
+4 -2
View File
@@ -22,8 +22,10 @@ struct SimpleXApp: App {
init() {
DispatchQueue.global(qos: .background).sync {
haskell_init()
// hs_init(0, nil)
// we have to use debug profile file name without extension here because .hp extension is added by profiler
// haskell_init(0, nil, nil)
let dummyHpPath = getAppDebugProfilePrefixPath().path + "_dummy" // not used, but required to populate eventlog
haskell_init(0, getAppEventLogPath().path, dummyHpPath)
}
UserDefaults.standard.register(defaults: appDefaults)
setGroupDefaults()
+4 -1
View File
@@ -17,6 +17,7 @@ struct ChatView: View {
@Environment(\.colorScheme) var colorScheme
@Environment(\.dismiss) var dismiss
@Environment(\.presentationMode) var presentationMode
@Environment(\.scenePhase) var scenePhase
@State @ObservedObject var chat: Chat
@State private var showChatInfoSheet: Bool = false
@State private var showAddMembersSheet: Bool = false
@@ -234,7 +235,9 @@ struct ChatView: View {
private func initChatView() {
let cInfo = chat.chatInfo
if case let .direct(contact) = cInfo {
// This check prevents the call to apiContactInfo after the app is suspended, and the database is closed.
if case .active = scenePhase,
case let .direct(contact) = cInfo {
Task {
do {
let (stats, _) = try await apiContactInfo(chat.chatInfo.apiId)
@@ -12,6 +12,7 @@ private let fillColorLight = Color(uiColor: UIColor(red: 0.99, green: 0.99, blue
struct UserPicker: View {
@EnvironmentObject var m: ChatModel
@Environment(\.colorScheme) var colorScheme
@Environment(\.scenePhase) var scenePhase
@Binding var showSettings: Bool
@Binding var showConnectDesktop: Bool
@Binding var userPickerVisible: Bool
@@ -91,7 +92,10 @@ struct UserPicker: View {
.opacity(userPickerVisible ? 1.0 : 0.0)
.onAppear {
do {
m.users = try listUsers()
// This check prevents the call of listUsers after the app is suspended, and the database is closed.
if case .active = scenePhase {
m.users = try listUsers()
}
} catch let error {
logger.error("Error loading users \(responseError(error))")
}
@@ -45,6 +45,11 @@ struct DeveloperView: View {
}
if developerTools {
Section {
exportDebugProfileButton()
exportEventLogButton()
}
Section {
settingsRow("key") {
Toggle("Post-quantum E2EE", isOn: $pqExperimentalEnabled)
@@ -62,6 +67,24 @@ 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)
@@ -396,7 +396,7 @@ func startChat() -> DBMigrationResult? {
startLock.wait()
defer { startLock.signal() }
if hasChatCtrl() {
return switch NSEChatState.shared.value {
case .created: doStartChat()
@@ -415,7 +415,8 @@ func startChat() -> DBMigrationResult? {
func doStartChat() -> DBMigrationResult? {
logger.debug("NotificationService: doStartChat")
haskell_init_nse()
haskell_init(1, nil, getAppDebugProfilePrefixPath().path)
// haskell_init(1, nil, nil)
let (_, dbStatus) = chatMigrateInit(confirmMigrations: defaultMigrationConfirmation(), backgroundMode: true)
logger.debug("NotificationService: doStartChat \(String(describing: dbStatus))")
if dbStatus != .ok {
+23 -20
View File
@@ -18,6 +18,11 @@
18415FEFE153C5920BFB7828 /* GroupWelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1841516F0CE5992B0EDFB377 /* GroupWelcomeView.swift */; };
3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */; };
3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4727FF621E00354CDD /* CILinkView.swift */; };
411D3A7C2BB9601D003D9A22 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 411D3A772BB9601D003D9A22 /* libgmp.a */; };
411D3A7D2BB9601D003D9A22 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 411D3A782BB9601D003D9A22 /* libffi.a */; };
411D3A7E2BB9601D003D9A22 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 411D3A792BB9601D003D9A22 /* libgmpxx.a */; };
411D3A7F2BB9601D003D9A22 /* libHSsimplex-chat-5.6.0.4-FOF2McwHkk1EIlP5UNozOv-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 411D3A7A2BB9601D003D9A22 /* libHSsimplex-chat-5.6.0.4-FOF2McwHkk1EIlP5UNozOv-ghc9.6.3.a */; };
411D3A802BB9601D003D9A22 /* libHSsimplex-chat-5.6.0.4-FOF2McwHkk1EIlP5UNozOv.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 411D3A7B2BB9601D003D9A22 /* libHSsimplex-chat-5.6.0.4-FOF2McwHkk1EIlP5UNozOv.a */; };
5C00164428A26FBC0094D739 /* ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C00164328A26FBC0094D739 /* ContextMenu.swift */; };
5C00168128C4FE760094D739 /* KeyChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C00168028C4FE760094D739 /* KeyChain.swift */; };
5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA72837DBB3004A9677 /* CICallItemView.swift */; };
@@ -36,11 +41,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 */; };
@@ -263,6 +263,11 @@
18415FD2E36F13F596A45BB4 /* CIVideoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CIVideoView.swift; sourceTree = "<group>"; };
3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeLinkView.swift; sourceTree = "<group>"; };
3CDBCF4727FF621E00354CDD /* CILinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CILinkView.swift; sourceTree = "<group>"; };
411D3A772BB9601D003D9A22 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgmp.a; path = ios/libgmp.a; sourceTree = "<group>"; };
411D3A782BB9601D003D9A22 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libffi.a; path = ios/libffi.a; sourceTree = "<group>"; };
411D3A792BB9601D003D9A22 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgmpxx.a; path = ios/libgmpxx.a; sourceTree = "<group>"; };
411D3A7A2BB9601D003D9A22 /* libHSsimplex-chat-5.6.0.4-FOF2McwHkk1EIlP5UNozOv-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libHSsimplex-chat-5.6.0.4-FOF2McwHkk1EIlP5UNozOv-ghc9.6.3.a"; path = "ios/libHSsimplex-chat-5.6.0.4-FOF2McwHkk1EIlP5UNozOv-ghc9.6.3.a"; sourceTree = "<group>"; };
411D3A7B2BB9601D003D9A22 /* libHSsimplex-chat-5.6.0.4-FOF2McwHkk1EIlP5UNozOv.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libHSsimplex-chat-5.6.0.4-FOF2McwHkk1EIlP5UNozOv.a"; path = "ios/libHSsimplex-chat-5.6.0.4-FOF2McwHkk1EIlP5UNozOv.a"; sourceTree = "<group>"; };
5C00164328A26FBC0094D739 /* ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenu.swift; sourceTree = "<group>"; };
5C00168028C4FE760094D739 /* KeyChain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyChain.swift; sourceTree = "<group>"; };
5C029EA72837DBB3004A9677 /* CICallItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CICallItemView.swift; sourceTree = "<group>"; };
@@ -290,11 +295,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>"; };
@@ -521,13 +521,13 @@
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 */,
411D3A7E2BB9601D003D9A22 /* libgmpxx.a in Frameworks */,
411D3A7C2BB9601D003D9A22 /* libgmp.a in Frameworks */,
411D3A802BB9601D003D9A22 /* libHSsimplex-chat-5.6.0.4-FOF2McwHkk1EIlP5UNozOv.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
411D3A7F2BB9601D003D9A22 /* libHSsimplex-chat-5.6.0.4-FOF2McwHkk1EIlP5UNozOv-ghc9.6.3.a in Frameworks */,
411D3A7D2BB9601D003D9A22 /* libffi.a 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 */,
411D3A782BB9601D003D9A22 /* libffi.a */,
411D3A772BB9601D003D9A22 /* libgmp.a */,
411D3A792BB9601D003D9A22 /* libgmpxx.a */,
411D3A7A2BB9601D003D9A22 /* libHSsimplex-chat-5.6.0.4-FOF2McwHkk1EIlP5UNozOv-ghc9.6.3.a */,
411D3A7B2BB9601D003D9A22 /* libHSsimplex-chat-5.6.0.4-FOF2McwHkk1EIlP5UNozOv.a */,
);
path = Libraries;
sourceTree = "<group>";
@@ -1576,6 +1576,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 204;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -1689,6 +1690,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 204;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
@@ -1767,6 +1769,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 204;
DEBUG_INFORMATION_FORMAT = dwarf;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
-11
View File
@@ -557,11 +557,6 @@ public enum ChatResponse: Decodable, Error {
case contactRequestRejected(user: UserRef)
case contactUpdated(user: UserRef, toContact: Contact)
case groupMemberUpdated(user: UserRef, groupInfo: GroupInfo, fromMember: GroupMember, toMember: GroupMember)
// TODO remove events below
case contactsSubscribed(server: String, contactRefs: [ContactRef])
case contactsDisconnected(server: String, contactRefs: [ContactRef])
case contactSubSummary(user: UserRef, contactSubscriptions: [ContactSubStatus])
// TODO remove events above
case networkStatus(networkStatus: NetworkStatus, connections: [String])
case networkStatuses(user_: UserRef?, networkStatuses: [ConnNetworkStatus])
case groupSubscribed(user: UserRef, groupInfo: GroupRef)
@@ -724,9 +719,6 @@ public enum ChatResponse: Decodable, Error {
case .contactRequestRejected: return "contactRequestRejected"
case .contactUpdated: return "contactUpdated"
case .groupMemberUpdated: return "groupMemberUpdated"
case .contactsSubscribed: return "contactsSubscribed"
case .contactsDisconnected: return "contactsDisconnected"
case .contactSubSummary: return "contactSubSummary"
case .networkStatus: return "networkStatus"
case .networkStatuses: return "networkStatuses"
case .groupSubscribed: return "groupSubscribed"
@@ -885,9 +877,6 @@ public enum ChatResponse: Decodable, Error {
case .contactRequestRejected: return noDetails
case let .contactUpdated(u, toContact): return withUser(u, String(describing: toContact))
case let .groupMemberUpdated(u, groupInfo, fromMember, toMember): return withUser(u, "groupInfo: \(groupInfo)\nfromMember: \(fromMember)\ntoMember: \(toMember)")
case let .contactsSubscribed(server, contactRefs): return "server: \(server)\ncontacts:\n\(String(describing: contactRefs))"
case let .contactsDisconnected(server, contactRefs): return "server: \(server)\ncontacts:\n\(String(describing: contactRefs))"
case let .contactSubSummary(u, contactSubscriptions): return withUser(u, String(describing: contactSubscriptions))
case let .networkStatus(status, conns): return "networkStatus: \(String(describing: status))\nconnections: \(String(describing: conns))"
case let .networkStatuses(u, statuses): return withUser(u, String(describing: statuses))
case let .groupSubscribed(u, groupInfo): return withUser(u, String(describing: groupInfo))
+16
View File
@@ -52,6 +52,10 @@ 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)
}
@@ -62,6 +66,18 @@ 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)
+45 -13
View File
@@ -7,32 +7,64 @@
//
#include "hs_init.h"
#include <string.h>
extern void hs_init_with_rtsopts(int * argc, char **argv[]);
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
};
void haskell_init(int nse, 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
if (nse) {
argv[argc++] = "simplex-nse"; // fake program name
} else {
argv[argc++] = "simplex";
}
argv[argc++] = "+RTS"; // start adding RTS options
if (nse) {
argv[argc++] = "-S"; // spam stdout with GC stats
argv[argc++] = "-A1m"; // chunk size for new allocations (less frequent GC)
argv[argc++] = "-H2m"; // larger heap size on start (faster boot)
argv[argc++] = "-M12m"; // hard limit on heap
argv[argc++] = "-F0.5"; // heap growth triggering GC
argv[argc++] = "-Fd1"; // memory return
} else {
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
}
if (nse) {
argv[argc++] = "-c"; // compacting garbage collector
} else {
int non_moving_gc = !heap_profile; // not compatible with heap profile
if (non_moving_gc) argv[argc++] = "-xn";
}
// wrap args as expected by RTS
char **pargv = argv;
hs_init_with_rtsopts(&argc, &pargv);
}
void haskell_init_nse(void) {
int argc = 9;
int argc = 7;
char *argv[] = {
"simplex",
"+RTS", // requires `hs_init_with_rtsopts`
"-A1m", // chunk size for new allocations
"-H1m", // initial heap size
"-M12m", // maximum heap size (25 total - 1 grace - 12 for swift), make GC work harder when approaching the limit
"-Mgrace=1m", // (default, just to make explicit) extra memory to handle "memory exhausted" exception and fail cleanly
"-F0.5", // heap growth triggering GC
"-Fd1", // memory return
"-c", // compacting garbage collector
+1 -1
View File
@@ -9,7 +9,7 @@
#ifndef hs_init_h
#define hs_init_h
void haskell_init(void);
void haskell_init(int nse, const char *eventlog, const char *heap_profile);
void haskell_init_nse(void);
@@ -143,6 +143,9 @@ dependencies {
implementation("com.jakewharton:process-phoenix:2.2.0")
//Camera Permission
implementation("com.google.accompanist:accompanist-permissions:0.23.0")
//implementation("androidx.compose.material:material-icons-extended:$compose_version")
//implementation("androidx.compose.ui:ui-util:$compose_version")
@@ -1,7 +1,9 @@
package chat.simplex.app.views.call
import android.Manifest
import android.app.*
import android.content.*
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Rect
import android.os.*
@@ -28,6 +30,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import chat.simplex.app.*
import chat.simplex.app.R
@@ -36,10 +39,12 @@ import chat.simplex.app.model.NtfManager
import chat.simplex.app.model.NtfManager.AcceptCallAction
import chat.simplex.common.model.*
import chat.simplex.common.platform.*
import chat.simplex.common.platform.chatModel
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.launch
import kotlinx.datetime.Clock
@@ -109,9 +114,20 @@ class CallActivity: ComponentActivity(), ServiceConnection {
m.callCommand.add(WCallCommand.Layout(layoutType))
}
private fun hasGrantedPermissions(): Boolean {
val grantedAudio = ContextCompat.checkSelfPermission(this, android.Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
val grantedCamera = !callSupportsVideo() || ContextCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
return grantedAudio && grantedCamera
}
override fun onBackPressed() {
if (isOnLockScreenNow()) {
super.onBackPressed()
} else if (!hasGrantedPermissions() && !callSupportsVideo()) {
val call = m.activeCall.value
if (call != null) {
withBGApi { chatModel.callManager.endCall(call) }
}
} else {
m.activeCallViewIsCollapsed.value = true
}
@@ -223,8 +239,21 @@ fun CallActivityView() {
}
Box(Modifier.background(Color.Black)) {
if (call != null) {
val permissionsState = rememberMultiplePermissionsState(
permissions = if (callSupportsVideo()) {
listOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)
} else {
listOf(Manifest.permission.RECORD_AUDIO)
}
)
if (permissionsState.allPermissionsGranted) {
ActiveCallView()
} else {
CallPermissionsView(remember { m.activeCallViewIsCollapsed }.value, callSupportsVideo()) {
withBGApi { chatModel.callManager.endCall(call) }
}
}
val view = LocalView.current
ActiveCallView()
if (callSupportsVideo()) {
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) {
@@ -252,6 +281,9 @@ fun CallActivityView() {
}
}
}
if (!m.activeCallViewIsCollapsed.value) {
AlertManager.shared.showInView()
}
}
LaunchedEffect(call == null) {
if (call != null) {
@@ -90,6 +90,9 @@ kotlin {
implementation("androidx.camera:camera-camera2:${cameraXVersion}")
implementation("androidx.camera:camera-lifecycle:${cameraXVersion}")
implementation("androidx.camera:camera-view:${cameraXVersion}")
// Calls lifecycle listener
implementation("androidx.lifecycle:lifecycle-process:2.4.1")
}
}
val desktopMain by getting {
@@ -0,0 +1,30 @@
package chat.simplex.common.helpers
import android.content.*
import android.net.Uri
import android.provider.Settings
import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.AlertManager
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.res.MR
fun Context.openAppSettingsInSystem() {
Intent().apply {
action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
data = Uri.parse("package:${androidAppContext.packageName}")
try {
startActivity(this)
} catch (e: ActivityNotFoundException) {
Log.e(TAG, e.stackTraceToString())
}
}
}
fun Context.showAllowPermissionInSettingsAlert(action: () -> Unit = ::openAppSettingsInSystem) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.permissions_grant_in_settings),
text = generalGetString(MR.strings.permissions_find_in_settings_and_grant),
confirmText = generalGetString(MR.strings.permissions_open_settings),
onConfirm = action,
)
}
@@ -1,5 +1,7 @@
package chat.simplex.common.views.call
import SectionSpacer
import SectionView
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
@@ -24,17 +26,18 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.*
import androidx.webkit.WebViewAssetLoader
import androidx.webkit.WebViewClientCompat
import chat.simplex.common.helpers.showAllowPermissionInSettingsAlert
import chat.simplex.common.model.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.model.ChatModel
@@ -42,13 +45,12 @@ import chat.simplex.common.model.Contact
import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.google.accompanist.permissions.*
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.datetime.Clock
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
// Should be destroy()'ed and set as null when call is ended. Otherwise, it will be a leak
@@ -209,7 +211,6 @@ actual fun ActiveCallView() {
ActiveCallOverlay(call, chatModel, audioViaBluetooth)
}
}
val context = LocalContext.current
DisposableEffect(Unit) {
val activity = context as? Activity ?: return@DisposableEffect onDispose {}
@@ -510,34 +511,138 @@ private fun DisabledBackgroundCallsButton() {
// }
//}
@Composable
fun CallPermissionsView(pipActive: Boolean, hasVideo: Boolean, cancel: () -> Unit) {
val audioPermission = rememberPermissionState(Manifest.permission.RECORD_AUDIO)
val cameraPermission = rememberPermissionState(Manifest.permission.CAMERA)
val permissionsState = rememberMultiplePermissionsState(
permissions = if (hasVideo) {
listOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)
} else {
listOf(Manifest.permission.RECORD_AUDIO)
}
)
val context = LocalContext.current
val buttonEnabled = remember { mutableStateOf(true) }
LaunchedEffect(Unit) {
if (!pipActive) {
permissionsState.launchMultiplePermissionRequestWithFallback(buttonEnabled, context::showAllowPermissionInSettingsAlert)
}
}
if (pipActive) {
Column(Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceEvenly) {
if (audioPermission.status is PermissionStatus.Denied) {
Icon(
painterResource(MR.images.ic_call_500),
stringResource(MR.strings.permissions_record_audio),
Modifier.size(24.dp),
tint = Color(0xFFFFFFD8)
)
}
if (hasVideo && cameraPermission.status is PermissionStatus.Denied) {
Icon(
painterResource(MR.images.ic_videocam),
stringResource(MR.strings.permissions_camera),
Modifier.size(24.dp),
tint = Color(0xFFFFFFD8)
)
}
}
} else {
ColumnWithScrollBar(Modifier.fillMaxSize()) {
Spacer(Modifier.height(AppBarHeight))
AppBarTitle(stringResource(MR.strings.permissions_required))
Spacer(Modifier.weight(1f))
val onClick = {
if (permissionsState.shouldShowRationale) {
context.showAllowPermissionInSettingsAlert()
} else {
permissionsState.launchMultiplePermissionRequestWithFallback(buttonEnabled, context::showAllowPermissionInSettingsAlert)
}
}
Text(stringResource(MR.strings.permissions_grant), Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING), textAlign = TextAlign.Center, color = Color(0xFFFFFFD8))
SectionSpacer()
SectionView {
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
val text = if (hasVideo && audioPermission.status is PermissionStatus.Denied && cameraPermission.status is PermissionStatus.Denied) {
stringResource(MR.strings.permissions_camera_and_record_audio)
} else if (audioPermission.status is PermissionStatus.Denied) {
stringResource(MR.strings.permissions_record_audio)
} else if (hasVideo && cameraPermission.status is PermissionStatus.Denied) {
stringResource(MR.strings.permissions_camera)
} else ""
GrantPermissionButton(text, buttonEnabled.value, onClick)
}
}
Spacer(Modifier.weight(1f))
Box(Modifier.fillMaxWidth().padding(bottom = if (hasVideo) 0.dp else DEFAULT_BOTTOM_PADDING), contentAlignment = Alignment.Center) {
SimpleButtonFrame(cancel, Modifier.height(64.dp)) {
Text(stringResource(MR.strings.call_service_notification_end_call), fontSize = 20.sp, color = Color(0xFFFFFFD8))
}
}
}
}
}
@Composable
private fun GrantPermissionButton(text: String, enabled: Boolean, onClick: () -> Unit) {
Row(
Modifier
.clickable(enabled = enabled, onClick = onClick)
.heightIn(min = 30.dp)
.background(WarningOrange.copy(0.3f), RoundedCornerShape(50)),
verticalAlignment = Alignment.CenterVertically
) {
Text(text, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), fontSize = 20.sp, color = WarningOrange)
}
}
/**
* The idea of this function is to ask system to show permission dialog and to see if it's really doing it.
* Otherwise, show alert with a button that opens settings for manual permission granting
* */
private fun MultiplePermissionsState.launchMultiplePermissionRequestWithFallback(buttonEnabled: MutableState<Boolean>, fallback: () -> Unit) {
buttonEnabled.value = false
val lifecycleOwner = ProcessLifecycleOwner.get().lifecycle
var useFallback = true
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_PAUSE) {
useFallback = false
buttonEnabled.value = true
}
}
lifecycleOwner.addObserver(observer)
withBGApi {
delay(2000)
if (useFallback && chatModel.activeCall.value != null) {
fallback()
}
buttonEnabled.value = true
}.invokeOnCompletion {
// Main thread only
withApi {
lifecycleOwner.removeObserver(observer)
}
}
launchMultiplePermissionRequest()
}
@Composable
fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIMessage) -> Unit) {
val webView = remember { mutableStateOf<WebView?>(null) }
val permissionsState = rememberMultiplePermissionsState(
permissions = listOf(
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.MODIFY_AUDIO_SETTINGS,
Manifest.permission.INTERNET
)
)
fun processCommand(wv: WebView, cmd: WCallCommand) {
val apiCall = WVAPICall(command = cmd)
wv.evaluateJavascript("processCommand(${json.encodeToString(apiCall)})", null)
}
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME || event == Lifecycle.Event.ON_START) {
permissionsState.launchMultiplePermissionRequest()
}
}
lifecycleOwner.lifecycle.addObserver(observer)
DisposableEffect(Unit) {
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
// val wv = webView.value
// if (wv != null) processCommand(wv, WCallCommand.End)
// webView.value?.destroy()
// val wv = webView.value
// if (wv != null) processCommand(wv, WCallCommand.End)
// webView.value?.destroy()
webView.value = null
}
}
@@ -560,44 +665,42 @@ fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIM
.addPathHandler("/assets/www/", WebViewAssetLoader.AssetsPathHandler(LocalContext.current))
.build()
if (permissionsState.allPermissionsGranted) {
Box(Modifier.fillMaxSize()) {
AndroidView(
factory = { AndroidViewContext ->
(staticWebView ?: WebView(androidAppContext)).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
)
this.webChromeClient = object: WebChromeClient() {
override fun onPermissionRequest(request: PermissionRequest) {
if (request.origin.toString().startsWith("file:/")) {
request.grant(request.resources)
} else {
Log.d(TAG, "Permission request from webview denied.")
request.deny()
}
Box(Modifier.fillMaxSize()) {
AndroidView(
factory = { AndroidViewContext ->
(staticWebView ?: WebView(androidAppContext)).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
)
this.webChromeClient = object: WebChromeClient() {
override fun onPermissionRequest(request: PermissionRequest) {
if (request.origin.toString().startsWith("file:/")) {
request.grant(request.resources)
} else {
Log.d(TAG, "Permission request from webview denied.")
request.deny()
}
}
this.webViewClient = LocalContentWebViewClient(webView, assetLoader)
this.clearHistory()
this.clearCache(true)
this.addJavascriptInterface(WebRTCInterface(onResponse), "WebRTCInterface")
val webViewSettings = this.settings
webViewSettings.allowFileAccess = true
webViewSettings.allowContentAccess = true
webViewSettings.javaScriptEnabled = true
webViewSettings.mediaPlaybackRequiresUserGesture = false
webViewSettings.cacheMode = WebSettings.LOAD_NO_CACHE
if (staticWebView == null) {
this.loadUrl("file:android_asset/www/android/call.html")
} else {
webView.value = this
}
}
this.webViewClient = LocalContentWebViewClient(webView, assetLoader)
this.clearHistory()
this.clearCache(true)
this.addJavascriptInterface(WebRTCInterface(onResponse), "WebRTCInterface")
val webViewSettings = this.settings
webViewSettings.allowFileAccess = true
webViewSettings.allowContentAccess = true
webViewSettings.javaScriptEnabled = true
webViewSettings.mediaPlaybackRequiresUserGesture = false
webViewSettings.cacheMode = WebSettings.LOAD_NO_CACHE
if (staticWebView == null) {
this.loadUrl("file:android_asset/www/android/call.html")
} else {
webView.value = this
}
}
) { /* WebView */ }
}
}
) { /* WebView */ }
}
}
@@ -1753,6 +1753,8 @@ object ChatController {
chatModel.removeChat(rhId, r.mergedContact.id)
}
}
// ContactsSubscribed, ContactsDisconnected and ContactSubSummary are only used in CLI,
// They have to be used here for remote desktop to process these status updates.
is CR.ContactsSubscribed -> updateContactsStatus(r.contactRefs, NetworkStatus.Connected())
is CR.ContactsDisconnected -> updateContactsStatus(r.contactRefs, NetworkStatus.Disconnected())
is CR.ContactSubSummary -> {
@@ -190,6 +190,7 @@ class AlertManager {
fun showAlertMsg(
title: String, text: String? = null,
confirmText: String = generalGetString(MR.strings.ok),
onConfirm: (() -> Unit)? = null,
hostDevice: Pair<Long?, String>? = null,
shareText: Boolean? = null
) {
@@ -220,6 +221,7 @@ class AlertManager {
}
TextButton(
onClick = {
onConfirm?.invoke()
hideAlert()
},
Modifier.focusRequester(focusRequester)
@@ -257,8 +259,9 @@ class AlertManager {
title: StringResource,
text: StringResource? = null,
confirmText: StringResource = MR.strings.ok,
onConfirm: (() -> Unit)? = null,
hostDevice: Pair<Long?, String>? = null,
) = showAlertMsg(generalGetString(title), if (text != null) generalGetString(text) else null, generalGetString(confirmText), hostDevice)
) = showAlertMsg(generalGetString(title), if (text != null) generalGetString(text) else null, generalGetString(confirmText), onConfirm, hostDevice)
@Composable
fun showInView() {
@@ -814,6 +814,14 @@
<!-- CallView -->
<string name="unable_to_open_browser_title">Error opening browser</string>
<string name="unable_to_open_browser_desc">The default web browser is required for calls. Please configure the default browser in the system, and share more information with the developers.</string>
<string name="permissions_required">Grant permissions</string>
<string name="permissions_record_audio">Microphone</string>
<string name="permissions_camera">Camera</string>
<string name="permissions_camera_and_record_audio">Camera and microphone</string>
<string name="permissions_grant">Grant permission(s) to make calls</string>
<string name="permissions_grant_in_settings">Grant in settings</string>
<string name="permissions_find_in_settings_and_grant">Find this permission in Android settings and grant it manually.</string>
<string name="permissions_open_settings">Open settings</string>
<!-- SimpleXInfo -->
<string name="next_generation_of_private_messaging">The next generation of private messaging</string>
+4 -57
View File
@@ -1,61 +1,8 @@
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns #-}
module Main where
import Control.Concurrent (forkIO, threadDelay)
import Control.Concurrent.STM
import Control.Monad
import Data.Time.Clock (getCurrentTime)
import Data.Time.LocalTime (getCurrentTimeZone)
import Server
import Simplex.Chat.Controller (ChatController (..), ChatResponse (..), currentRemoteHost, versionNumber, versionString)
import Simplex.Chat.Core
import Simplex.Chat.Options
import Simplex.Chat.Terminal
import Simplex.Chat.View (serializeChatResponse)
import Simplex.Messaging.Client (NetworkConfig (..))
import System.Directory (getAppUserDataDirectory)
import System.Terminal (withTerminal)
import Server (simplexChatServer)
import Simplex.Chat.Terminal (terminalChatConfig)
import Simplex.Chat.Terminal.Main (simplexChatCLI)
main :: IO ()
main = do
appDir <- getAppUserDataDirectory "simplex"
opts@ChatOpts {chatCmd, chatServerPort} <- getChatOpts appDir "simplex_v1"
if null chatCmd
then case chatServerPort of
Just chatPort -> simplexChatServer defaultChatServerConfig {chatPort} terminalChatConfig opts
_ -> runCLI opts
else simplexChatCore terminalChatConfig opts $ runCommand opts
where
runCLI opts = do
welcome opts
t <- withTerminal pure
simplexChatTerminal terminalChatConfig opts t
runCommand ChatOpts {chatCmd, chatCmdLog, chatCmdDelay} user cc = do
when (chatCmdLog /= CCLNone) . void . forkIO . forever $ do
(_, _, r') <- atomically . readTBQueue $ outputQ cc
case r' of
CRNewChatItem {} -> printResponse r'
_ -> when (chatCmdLog == CCLAll) $ printResponse r'
sendChatCmdStr cc chatCmd >>= printResponse
threadDelay $ chatCmdDelay * 1000000
where
printResponse r = do
ts <- getCurrentTime
tz <- getCurrentTimeZone
rh <- readTVarIO $ currentRemoteHost cc
putStrLn $ serializeChatResponse (rh, Just user) ts tz rh r
welcome :: ChatOpts -> IO ()
welcome ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, networkConfig}} =
mapM_
putStrLn
[ versionString versionNumber,
"db: " <> dbFilePrefix <> "_chat.db, " <> dbFilePrefix <> "_agent.db",
maybe
"direct network connection - use `/network` command or `-x` CLI option to connect via SOCKS5 at :9050"
(("using SOCKS5 proxy " <>) . show)
(socksProxy networkConfig),
"type \"/help\" or \"/h\" for usage info"
]
main = simplexChatCLI terminalChatConfig (Just simplexChatServer)
+3 -3
View File
@@ -28,9 +28,9 @@ import Simplex.Messaging.Util (raceAny_)
import UnliftIO.Exception
import UnliftIO.STM
simplexChatServer :: ChatServerConfig -> ChatConfig -> ChatOpts -> IO ()
simplexChatServer srvCfg cfg opts =
simplexChatCore cfg opts . const $ runChatServer srvCfg
simplexChatServer :: ServiceName -> ChatConfig -> ChatOpts -> IO ()
simplexChatServer chatPort cfg opts =
simplexChatCore cfg opts . const $ runChatServer defaultChatServerConfig {chatPort}
data ChatServerConfig = ChatServerConfig
{ chatPort :: ServiceName,
@@ -70,7 +70,7 @@ crDirectoryEvent = \case
CRChatItemDeleted {deletedChatItem = AChatItem _ SMDRcv (DirectChat ct) _, byUser = False} -> Just $ DEItemDeleteIgnored ct
CRNewChatItem {chatItem = AChatItem _ SMDRcv (DirectChat ct) ci@ChatItem {content = CIRcvMsgContent mc, meta = CIMeta {itemLive}}} ->
Just $ case (mc, itemLive) of
(MCText t, Nothing) -> DEContactCommand ct ciId $ fromRight err $ A.parseOnly directoryCmdP $ T.dropWhileEnd isSpace t
(MCText t, Nothing) -> DEContactCommand ct ciId $ fromRight err $ A.parseOnly (directoryCmdP <* A.endOfInput) $ T.dropWhileEnd isSpace t
_ -> DEUnsupportedMessage ct ciId
where
ciId = chatItemId' ci
+1 -1
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: 8b94264623eb8f32793a3b4129cb12770f54ef3a
source-repository-package
type: git
+6
View File
@@ -150,6 +150,7 @@ tests:
ghc-options:
# - -haddock
- -O2
- -Wall
- -Wcompat
- -Werror=incomplete-patterns
@@ -157,3 +158,8 @@ ghc-options:
- -Wincomplete-record-updates
- -Wincomplete-uni-patterns
- -Wunused-type-patterns
- -finfo-table-map
- -fdistinct-constructor-tables
default-extensions:
- StrictData
+1 -1
View File
@@ -1,5 +1,5 @@
{
"https://github.com/simplex-chat/simplexmq.git"."ee90ea6a69fe8283d37d9821cd83798fd0a76260" = "0my9f4dlfa79yq73rys0m2zb61fd9bp65djvavk6jwy6qzl5vr40";
"https://github.com/simplex-chat/simplexmq.git"."8b94264623eb8f32793a3b4129cb12770f54ef3a" = "0n4wwpkj9dps7ansvlrdf4kiwjczij7y79i4cnr3gr0lmml8bpbs";
"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";
+23 -7
View File
@@ -139,6 +139,7 @@ library
Simplex.Chat.Migrations.M20240226_users_restrict
Simplex.Chat.Migrations.M20240228_pq
Simplex.Chat.Migrations.M20240313_drop_agent_ack_cmd_id
Simplex.Chat.Migrations.M20240324_custom_data
Simplex.Chat.Mobile
Simplex.Chat.Mobile.File
Simplex.Chat.Mobile.Shared
@@ -168,6 +169,7 @@ library
Simplex.Chat.Styled
Simplex.Chat.Terminal
Simplex.Chat.Terminal.Input
Simplex.Chat.Terminal.Main
Simplex.Chat.Terminal.Notification
Simplex.Chat.Terminal.Output
Simplex.Chat.Types
@@ -179,7 +181,9 @@ 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 -finfo-table-map -fdistinct-constructor-tables
build-depends:
aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12
@@ -238,7 +242,9 @@ 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 -finfo-table-map -fdistinct-constructor-tables -threaded
build-depends:
aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12
@@ -298,7 +304,9 @@ 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 -finfo-table-map -fdistinct-constructor-tables -threaded
build-depends:
aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12
@@ -357,11 +365,13 @@ 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 -finfo-table-map -fdistinct-constructor-tables -threaded
build-depends:
aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12
@@ -422,7 +432,9 @@ 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 -finfo-table-map -fdistinct-constructor-tables -threaded
build-depends:
aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12
@@ -482,6 +494,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
@@ -489,7 +503,7 @@ 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 -finfo-table-map -fdistinct-constructor-tables -threaded
build-depends:
aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12
@@ -580,7 +594,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 -finfo-table-map -fdistinct-constructor-tables -threaded
build-depends:
QuickCheck ==2.14.*
, aeson ==2.2.*
+500 -487
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
+80 -34
View File
@@ -82,7 +82,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
@@ -144,9 +144,28 @@ data ChatConfig = ChatConfig
ciExpirationInterval :: Int64, -- microseconds
coreApi :: Bool,
highlyAvailable :: Bool,
deviceNameForRemote :: Text
deviceNameForRemote :: Text,
chatHooks :: ChatHooks
}
-- The hooks can be used to extend or customize chat core in mobile or CLI clients.
data ChatHooks = ChatHooks
{ -- preCmdHook can be used to process or modify the commands before they are processed.
-- This hook should be used to process CustomChatCommand.
-- if this hook returns ChatResponse, the command processing will be skipped.
preCmdHook :: ChatController -> ChatCommand -> IO (Either ChatResponse ChatCommand),
-- eventHook can be used to additionally process or modify events,
-- it is called before the event is sent to the user (or to the UI).
eventHook :: ChatController -> ChatResponse -> IO ChatResponse
}
defaultChatHooks :: ChatHooks
defaultChatHooks =
ChatHooks
{ preCmdHook = \_ -> pure . Right,
eventHook = \_ -> pure
}
data DefaultAgentServers = DefaultAgentServers
{ smp :: NonEmpty SMPServerWithAuth,
ntf :: [NtfServer],
@@ -471,6 +490,9 @@ data ChatCommand
| GetAgentSubsDetails
| GetAgentWorkers
| GetAgentWorkersDetails
-- The parser will return this command for strings that start from "//".
-- This command should be processed in preCmdHook
| CustomChatCommand ByteString
deriving (Show)
allowRemoteCommand :: ChatCommand -> Bool -- XXX: consider using Relay/Block/ForceLocal
@@ -597,10 +619,9 @@ data ChatResponse
| CRContactRequestAlreadyAccepted {user :: User, contact :: Contact}
| CRLeftMemberUser {user :: User, groupInfo :: GroupInfo}
| CRGroupDeletedUser {user :: User, groupInfo :: GroupInfo}
| CRRcvFileDescrReady {user :: User, chatItem :: AChatItem}
| CRRcvFileDescrReady {user :: User, chatItem :: AChatItem, rcvFileTransfer :: RcvFileTransfer, rcvFileDescr :: RcvFileDescr}
| CRRcvFileAccepted {user :: User, chatItem :: AChatItem}
| CRRcvFileAcceptedSndCancelled {user :: User, rcvFileTransfer :: RcvFileTransfer}
| CRRcvFileDescrNotReady {user :: User, chatItem :: AChatItem}
| CRStandaloneFileInfo {fileMeta :: Maybe J.Value}
| CRRcvStandaloneFileCreated {user :: User, rcvFileTransfer :: RcvFileTransfer} -- returned by _download
| CRRcvFileStart {user :: User, chatItem :: AChatItem} -- sent by chats
@@ -726,6 +747,7 @@ data ChatResponse
| CRArchiveImported {archiveErrors :: [ArchiveError]}
| CRAppSettings {appSettings :: AppSettings}
| CRTimedAction {action :: String, durationMilliseconds :: Int64}
| CRCustomChatResponse {user_ :: Maybe User, response :: Text}
deriving (Show)
-- some of these can only be used as command responses
@@ -1118,7 +1140,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
@@ -1222,39 +1244,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 #-}
@@ -1273,14 +1315,18 @@ 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 event = do
localQ <- asks outputQ
session <- asks remoteCtrlSession
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 $
readTVar session >>= \case
Just (_, RCSessionConnected {remoteOutputQ})
@@ -1288,15 +1334,15 @@ toView event = 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
liftEither =<< liftIO (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
@@ -1310,17 +1356,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
@@ -0,0 +1,20 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Migrations.M20240324_custom_data where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20240324_custom_data :: Query
m20240324_custom_data =
[sql|
ALTER TABLE contacts ADD COLUMN custom_data BLOB;
ALTER TABLE groups ADD COLUMN custom_data BLOB;
|]
down_m20240324_custom_data :: Query
down_m20240324_custom_data =
[sql|
ALTER TABLE contacts DROP COLUMN custom_data;
ALTER TABLE groups DROP COLUMN custom_data;
|]
+3 -1
View File
@@ -73,6 +73,7 @@ CREATE TABLE contacts(
REFERENCES group_members(group_member_id) ON DELETE SET NULL,
contact_grp_inv_sent INTEGER NOT NULL DEFAULT 0,
contact_status TEXT NOT NULL DEFAULT 'active',
custom_data BLOB,
FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name)
ON DELETE CASCADE
@@ -120,7 +121,8 @@ CREATE TABLE groups(
favorite INTEGER NOT NULL DEFAULT 0,
send_rcpts INTEGER,
via_group_link_uri_hash BLOB,
user_member_profile_sent_at TEXT, -- received
user_member_profile_sent_at TEXT,
custom_data BLOB, -- received
FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name)
ON DELETE CASCADE
+3 -10
View File
@@ -46,7 +46,7 @@ import Simplex.Chat.Store.Profiles
import Simplex.Chat.Types
import Simplex.Messaging.Agent.Client (agentClientStore)
import Simplex.Messaging.Agent.Env.SQLite (createAgentStore)
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, connectSQLiteStore, closeSQLiteStore, reopenSQLiteStore)
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
@@ -54,8 +54,6 @@ import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, sumTypeJSON)
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), BasicAuth (..), CorrId (..), ProtoServerWithAuth (..), ProtocolServer (..))
import Simplex.Messaging.Util (catchAll, liftEitherWith, safeDecodeUtf8)
import System.Directory
import System.FilePath
import System.IO (utf8)
import System.Timeout (timeout)
@@ -235,7 +233,7 @@ chatMigrateInit dbFilePrefix dbKey confirm = chatMigrateInitKey dbFilePrefix dbK
chatMigrateInitKey :: String -> ScrubbedBytes -> Bool -> String -> Bool -> IO (Either DBMigrationResult ChatController)
chatMigrateInitKey dbFilePrefix dbKey keepKey confirm backgroundMode = runExceptT $ do
confirmMigrations <- liftEitherWith (const DBMInvalidConfirmation) $ strDecode @MigrationConfirmation $ B.pack confirm
confirmMigrations <- liftEitherWith (const DBMInvalidConfirmation) $ strDecode $ B.pack confirm
chatStore <- migrate createChatStore (chatStoreFile dbFilePrefix) confirmMigrations
agentStore <- migrate createAgentStore (agentStoreFile dbFilePrefix) confirmMigrations
liftIO $ initialize chatStore ChatDatabase {chatStore, agentStore}
@@ -245,15 +243,10 @@ chatMigrateInitKey dbFilePrefix dbKey keepKey confirm backgroundMode = runExcept
newChatController db user_ defaultMobileConfig (mobileChatOpts dbFilePrefix) backgroundMode
migrate createStore dbFile confirmMigrations =
ExceptT $
(first (DBMErrorMigration dbFile) <$> createStore' dbFile dbKey keepKey confirmMigrations)
(first (DBMErrorMigration dbFile) <$> createStore dbFile dbKey keepKey confirmMigrations)
`catch` (pure . checkDBError)
`catchAll` (pure . dbError)
where
createStore' dbFilePath dbKey keepKey _confirmMigrations = do
let dbDir = takeDirectory dbFilePath
createDirectoryIfMissing True dbDir
Right <$> connectSQLiteStore dbFilePath dbKey keepKey
checkDBError e = case sqlError e of
DB.ErrorNotADatabase -> Left $ DBMErrorNotADatabase dbFile
_ -> dbError e
+71 -67
View File
@@ -49,7 +49,7 @@ 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))
@@ -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
liftEither =<< liftIO (runExceptT $ withExceptT (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,11 +489,11 @@ 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
@@ -508,15 +508,15 @@ 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} -> 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 :: Respond
replyWith rr attach = do
resp <- liftRC $ encryptEncodeHTTP2Body encryption $ J.encode rr
liftIO . sendResponse . responseStreaming N.status200 [] $ \send flush -> do
@@ -524,51 +524,55 @@ handleRemoteCommand execChatCommand encryption remoteOutputQ HTTP2Request {reque
attach send
flush
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 <$> lift (execChatCommand $ encodeUtf8 command) `catchChatError'` (pure . CRChatError Nothing)
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}
@@ -583,7 +587,7 @@ handleGetFile encryption User {userId} RemoteFile {userId = commandUserId, fileI
encFile <- liftRC $ prepareEncryptedFile encryption (h, fileSize)
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 +608,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
@@ -626,7 +630,7 @@ verifyRemoteCtrlSession execChatCommand sessCode' = do
_ -> 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 +639,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 +656,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 +689,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
+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
+5 -5
View File
@@ -75,19 +75,19 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do
[sql|
SELECT
c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, c.via_group, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite,
p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.contact_group_member_id, c.contact_grp_inv_sent
p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.contact_group_member_id, c.contact_grp_inv_sent, c.custom_data
FROM contacts c
JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id
WHERE c.user_id = ? AND c.contact_id = ? AND c.deleted = 0
|]
(userId, contactId)
toContact' :: Int64 -> Connection -> [(ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Int64, Bool, ContactStatus) :. (Maybe MsgFilter, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool)] -> Either StoreError Contact
toContact' contactId conn [(profileId, localDisplayName, displayName, fullName, image, contactLink, localAlias, viaGroup, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent)] =
toContact' :: Int64 -> Connection -> [(ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Int64, Bool, ContactStatus) :. (Maybe MsgFilter, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool, Maybe CustomData)] -> Either StoreError Contact
toContact' contactId conn [(profileId, localDisplayName, displayName, fullName, image, contactLink, localAlias, viaGroup, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, customData)] =
let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite}
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn
activeConn = Just conn
in Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent}
in Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, customData}
toContact' _ _ _ = Left $ SEInternalError "referenced contact not found"
getGroupAndMember_ :: Int64 -> Connection -> ExceptT StoreError IO (GroupInfo, GroupMember)
getGroupAndMember_ groupMemberId c = ExceptT $ do
@@ -99,7 +99,7 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image,
g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.custom_data,
-- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
+11 -5
View File
@@ -68,6 +68,7 @@ module Simplex.Chat.Store.Direct
updateContactSettings,
setConnConnReqInv,
resetContactConnInitiated,
setContactCustomData,
)
where
@@ -175,7 +176,7 @@ getContactByConnReqHash db vr user@User {userId} cReqHash =
SELECT
-- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.custom_data,
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
@@ -221,7 +222,7 @@ createDirectContact db user@User {userId} conn@Connection {connId, localAlias} p
let profile = toLocalProfile profileId p localAlias
userPreferences = emptyChatPrefs
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn
pure $ Contact {contactId, localDisplayName, profile, activeConn = Just conn, viaGroup = Nothing, contactUsed, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Nothing, contactGrpInvSent = False}
pure $ Contact {contactId, localDisplayName, profile, activeConn = Just conn, viaGroup = Nothing, contactUsed, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Nothing, contactGrpInvSent = False, customData = Nothing}
deleteContactConnectionsAndFiles :: DB.Connection -> UserId -> Contact -> IO ()
deleteContactConnectionsAndFiles db userId Contact {contactId} = do
@@ -578,7 +579,7 @@ createOrUpdateContactRequest db vr user@User {userId} userContactLinkId invId (V
SELECT
-- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.custom_data,
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
@@ -724,7 +725,7 @@ createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}
contactId <- insertedRowId db
conn <- createConnection_ db userId ConnContact (Just contactId) agentConnId connChatVersion cReqChatVRange Nothing (Just userContactLinkId) customUserProfileId 0 createdAt subMode pqSup
let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn
pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile "", activeConn = Just conn, viaGroup = Nothing, contactUsed, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = createdAt, updatedAt = createdAt, chatTs = Just createdAt, contactGroupMemberId = Nothing, contactGrpInvSent = False}
pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile "", activeConn = Just conn, viaGroup = Nothing, contactUsed, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = createdAt, updatedAt = createdAt, chatTs = Just createdAt, contactGroupMemberId = Nothing, contactGrpInvSent = False, customData = Nothing}
getContactIdByName :: DB.Connection -> User -> ContactName -> ExceptT StoreError IO Int64
getContactIdByName db User {userId} cName =
@@ -743,7 +744,7 @@ getContact_ db vr user@User {userId} contactId deleted =
SELECT
-- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.custom_data,
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
@@ -883,3 +884,8 @@ resetContactConnInitiated db User {userId} Connection {connId} = do
WHERE user_id = ? AND connection_id = ?
|]
(updatedAt, userId, connId)
setContactCustomData :: DB.Connection -> User -> Contact -> Maybe CustomData -> IO ()
setContactCustomData db User {userId} Contact {contactId} customData = do
updatedAt <- getCurrentTime
DB.execute db "UPDATE contacts SET custom_data = ?, updated_at = ? WHERE user_id = ? AND contact_id = ?" (customData, updatedAt, userId, contactId)
+19 -11
View File
@@ -116,6 +116,7 @@ module Simplex.Chat.Store.Groups
createNewUnknownGroupMember,
updateUnknownMemberAnnounced,
updateUserMemberProfileSentAt,
setGroupCustomData,
)
where
@@ -148,19 +149,19 @@ import Simplex.Messaging.Util (eitherToMaybe, ($>>=), (<$$>))
import Simplex.Messaging.Version
import UnliftIO.STM
type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Maybe ImageData, Maybe ProfileId, Maybe MsgFilter, Maybe Bool, Bool, Maybe GroupPreferences) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. GroupMemberRow
type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Maybe ImageData, Maybe ProfileId, Maybe MsgFilter, Maybe Bool, Bool, Maybe GroupPreferences) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime, Maybe CustomData) :. GroupMemberRow
type GroupMemberRow = ((Int64, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, Bool, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences))
type MaybeGroupMemberRow = ((Maybe Int64, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe Bool, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId, Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe ImageData, Maybe ConnReqContact, Maybe LocalAlias, Maybe Preferences))
toGroupInfo :: (PQSupport -> VersionRangeChat) -> Int64 -> GroupInfoRow -> GroupInfo
toGroupInfo vr userContactId ((groupId, localDisplayName, displayName, fullName, description, image, hostConnCustomUserProfileId, enableNtfs_, sendRcpts, favorite, groupPreferences) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. userMemberRow) =
toGroupInfo vr userContactId ((groupId, localDisplayName, displayName, fullName, description, image, hostConnCustomUserProfileId, enableNtfs_, sendRcpts, favorite, groupPreferences) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt, customData) :. userMemberRow) =
let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = vr PQSupportOff}
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite}
fullGroupPreferences = mergeGroupPreferences groupPreferences
groupProfile = GroupProfile {displayName, fullName, description, image, groupPreferences}
in GroupInfo {groupId, localDisplayName, groupProfile, fullGroupPreferences, membership, hostConnCustomUserProfileId, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt}
in GroupInfo {groupId, localDisplayName, groupProfile, fullGroupPreferences, membership, hostConnCustomUserProfileId, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, customData}
toGroupMember :: Int64 -> GroupMemberRow -> GroupMember
toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberRestriction_) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, preferences)) =
@@ -271,7 +272,7 @@ getGroupAndMember db User {userId, userContactId} groupMemberId vr =
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image,
g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.custom_data,
-- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
@@ -344,7 +345,8 @@ createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile = Exc
createdAt = currentTs,
updatedAt = currentTs,
chatTs = Just currentTs,
userMemberProfileSentAt = Just currentTs
userMemberProfileSentAt = Just currentTs,
customData = Nothing
}
-- | creates a new group record for the group the current user was invited to, or returns an existing one
@@ -409,7 +411,8 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ
createdAt = currentTs,
updatedAt = currentTs,
chatTs = Just currentTs,
userMemberProfileSentAt = Just currentTs
userMemberProfileSentAt = Just currentTs,
customData = Nothing
},
groupMemberId
)
@@ -628,7 +631,7 @@ getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ =
SELECT
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image,
g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.custom_data,
mu.group_member_id, g.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction,
mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences
FROM groups g
@@ -1293,7 +1296,7 @@ getViaGroupMember db vr User {userId, userContactId} Contact {contactId} =
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image,
g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.custom_data,
-- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
@@ -1389,7 +1392,7 @@ getGroupInfo db vr User {userId, userContactId} groupId =
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image,
g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.custom_data,
-- GroupMember - membership
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
@@ -1951,7 +1954,7 @@ createMemberContact
authErrCounter = 0
}
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn
pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False}
pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, customData = Nothing}
getMemberContact :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> ContactId -> ExceptT StoreError IO (GroupInfo, GroupMember, Contact, ConnReqInvitation)
getMemberContact db vr user contactId = do
@@ -1988,7 +1991,7 @@ createMemberContactInvited
contactId <- createContactUpdateMember currentTs userPreferences
ctConn <- createMemberContactConn_ db user connIds gInfo mConn contactId subMode
let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn
mCt' = Contact {contactId, localDisplayName = memberLDN, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Nothing, contactGrpInvSent = False}
mCt' = Contact {contactId, localDisplayName = memberLDN, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Nothing, contactGrpInvSent = False, customData = Nothing}
m' = m {memberContactId = Just contactId}
pure (mCt', m')
where
@@ -2188,3 +2191,8 @@ updateUserMemberProfileSentAt db User {userId} GroupInfo {groupId} sentTs =
db
"UPDATE groups SET user_member_profile_sent_at = ? WHERE user_id = ? AND group_id = ?"
(sentTs, userId, groupId)
setGroupCustomData :: DB.Connection -> User -> GroupInfo -> Maybe CustomData -> IO ()
setGroupCustomData db User {userId} GroupInfo {groupId} customData = do
updatedAt <- getCurrentTime
DB.execute db "UPDATE groups SET custom_data = ?, updated_at = ? WHERE user_id = ? AND group_id = ?" (customData, updatedAt, userId, groupId)
+10
View File
@@ -41,6 +41,7 @@ module Simplex.Chat.Store.Messages
getDirectChatItemLast,
getAllChatItems,
getAChatItem,
getAChatItemBySharedMsgId,
updateDirectChatItem,
updateDirectChatItem',
addInitialAndNewCIVersions,
@@ -2202,6 +2203,15 @@ getAChatItem db vr user chatRef itemId = case chatRef of
pure $ AChatItem SCTLocal msgDir (LocalChat nf) ci
_ -> throwError $ SEChatItemNotFound itemId
getAChatItemBySharedMsgId :: ChatTypeQuotable c => DB.Connection -> User -> ChatDirection c 'MDRcv -> SharedMsgId -> ExceptT StoreError IO AChatItem
getAChatItemBySharedMsgId db user cd sharedMsgId = case cd of
CDDirectRcv ct@Contact {contactId} -> do
(CChatItem msgDir ci) <- getDirectChatItemBySharedMsgId db user contactId sharedMsgId
pure $ AChatItem SCTDirect msgDir (DirectChat ct) ci
CDGroupRcv g@GroupInfo {groupId} GroupMember {groupMemberId} -> do
(CChatItem msgDir ci) <- getGroupChatItemBySharedMsgId db user groupId groupMemberId sharedMsgId
pure $ AChatItem SCTGroup msgDir (GroupChat g) ci
getChatItemVersions :: DB.Connection -> ChatItemId -> IO [ChatItemVersion]
getChatItemVersions db itemId = do
map toChatItemVersion
+3 -1
View File
@@ -103,6 +103,7 @@ import Simplex.Chat.Migrations.M20240222_app_settings
import Simplex.Chat.Migrations.M20240226_users_restrict
import Simplex.Chat.Migrations.M20240228_pq
import Simplex.Chat.Migrations.M20240313_drop_agent_ack_cmd_id
import Simplex.Chat.Migrations.M20240324_custom_data
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)]
@@ -205,7 +206,8 @@ schemaMigrations =
("20240222_app_settings", m20240222_app_settings, Just down_m20240222_app_settings),
("20240226_users_restrict", m20240226_users_restrict, Just down_m20240226_users_restrict),
("20240228_pq", m20240228_pq, Just down_m20240228_pq),
("20240313_drop_agent_ack_cmd_id", m20240313_drop_agent_ack_cmd_id, Just down_m20240313_drop_agent_ack_cmd_id)
("20240313_drop_agent_ack_cmd_id", m20240313_drop_agent_ack_cmd_id, Just down_m20240313_drop_agent_ack_cmd_id),
("20240324_custom_data", m20240324_custom_data, Just down_m20240324_custom_data)
]
-- | The list of migrations in ascending order by date
+3 -3
View File
@@ -371,16 +371,16 @@ deleteUnusedIncognitoProfileById_ db User {userId} profileId =
|]
[":user_id" := userId, ":profile_id" := profileId]
type ContactRow = (ContactId, ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Bool, ContactStatus) :. (Maybe MsgFilter, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool)
type ContactRow = (ContactId, ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Bool, ContactStatus) :. (Maybe MsgFilter, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool, Maybe CustomData)
toContact :: (PQSupport -> VersionRangeChat) -> User -> ContactRow :. MaybeConnectionRow -> Contact
toContact vr user (((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent)) :. connRow) =
toContact vr user (((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, customData)) :. connRow) =
let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
activeConn = toMaybeConnection vr connRow
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite}
incognito = maybe False connIncognito activeConn
mergedPreferences = contactUserPreferences user userPreferences preferences incognito
in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent}
in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, customData}
getProfileById :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO LocalProfile
getProfileById db userId profileId =
+64
View File
@@ -0,0 +1,64 @@
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns #-}
module Simplex.Chat.Terminal.Main where
import Control.Concurrent (forkIO, threadDelay)
import Control.Concurrent.STM
import Control.Monad
import Data.Time.Clock (getCurrentTime)
import Data.Time.LocalTime (getCurrentTimeZone)
import Network.Socket
import Simplex.Chat.Controller (ChatConfig, ChatController (..), ChatResponse (..), currentRemoteHost, versionNumber, versionString)
import Simplex.Chat.Core
import Simplex.Chat.Options
import Simplex.Chat.Terminal
import Simplex.Chat.View (serializeChatResponse)
import Simplex.Messaging.Client (NetworkConfig (..))
import System.Directory (getAppUserDataDirectory)
import System.Exit (exitFailure)
import System.Terminal (withTerminal)
simplexChatCLI :: ChatConfig -> Maybe (ServiceName -> ChatConfig -> ChatOpts -> IO ()) -> IO ()
simplexChatCLI cfg server_ = do
appDir <- getAppUserDataDirectory "simplex"
opts@ChatOpts {chatCmd, chatServerPort} <- getChatOpts appDir "simplex_v1"
if null chatCmd
then case chatServerPort of
Just chatPort -> case server_ of
Just server -> server chatPort cfg opts
Nothing -> putStrLn "Not allowed to run as a WebSockets server" >> exitFailure
_ -> runCLI opts
else simplexChatCore cfg opts $ runCommand opts
where
runCLI opts = do
welcome opts
t <- withTerminal pure
simplexChatTerminal cfg opts t
runCommand ChatOpts {chatCmd, chatCmdLog, chatCmdDelay} user cc = do
when (chatCmdLog /= CCLNone) . void . forkIO . forever $ do
(_, _, r') <- atomically . readTBQueue $ outputQ cc
case r' of
CRNewChatItem {} -> printResponse r'
_ -> when (chatCmdLog == CCLAll) $ printResponse r'
sendChatCmdStr cc chatCmd >>= printResponse
threadDelay $ chatCmdDelay * 1000000
where
printResponse r = do
ts <- getCurrentTime
tz <- getCurrentTimeZone
rh <- readTVarIO $ currentRemoteHost cc
putStrLn $ serializeChatResponse (rh, Just user) ts tz rh r
welcome :: ChatOpts -> IO ()
welcome ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, networkConfig}} =
mapM_
putStrLn
[ versionString versionNumber,
"db: " <> dbFilePrefix <> "_chat.db, " <> dbFilePrefix <> "_agent.db",
maybe
"direct network connection - use `/network` command or `-x` CLI option to connect via SOCKS5 at :9050"
(("using SOCKS5 proxy " <>) . show)
(socksProxy networkConfig),
"type \"/help\" or \"/h\" for usage info"
]
+18 -2
View File
@@ -174,10 +174,25 @@ data Contact = Contact
updatedAt :: UTCTime,
chatTs :: Maybe UTCTime,
contactGroupMemberId :: Maybe GroupMemberId,
contactGrpInvSent :: Bool
contactGrpInvSent :: Bool,
customData :: Maybe CustomData
}
deriving (Eq, Show)
newtype CustomData = CustomData J.Object
deriving (Eq, Show)
instance ToJSON CustomData where
toJSON (CustomData v) = toJSON v
toEncoding (CustomData v) = toEncoding v
instance FromJSON CustomData where
parseJSON = J.withObject "CustomData" (pure . CustomData)
instance ToField CustomData where toField (CustomData v) = toField $ J.encode v
instance FromField CustomData where fromField = fromBlobField_ J.eitherDecodeStrict
contactConn :: Contact -> Maybe Connection
contactConn Contact {activeConn} = activeConn
@@ -356,7 +371,8 @@ data GroupInfo = GroupInfo
createdAt :: UTCTime,
updatedAt :: UTCTime,
chatTs :: Maybe UTCTime,
userMemberProfileSentAt :: Maybe UTCTime
userMemberProfileSentAt :: Maybe UTCTime,
customData :: Maybe CustomData
}
deriving (Eq, Show)
+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 #-}
+9 -4
View File
@@ -183,8 +183,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
CRUnknownMemberBlocked u g byM um -> ttyUser u [ttyGroup' g <> ": " <> ttyMember byM <> " blocked an unknown member, creating unknown member record " <> ttyMember um]
CRUnknownMemberAnnounced u g _ um m -> ttyUser u [ttyGroup' g <> ": unknown member " <> ttyMember um <> " updated to " <> ttyMember m]
CRGroupDeletedUser u g -> ttyUser u [ttyGroup' g <> ": you deleted the group"]
CRRcvFileDescrReady _ _ -> []
CRRcvFileDescrNotReady _ _ -> []
CRRcvFileDescrReady _ _ _ _ -> []
CRRcvFileProgressXFTP {} -> []
CRRcvFileAccepted u ci -> ttyUser u $ savingFile' ci
CRRcvFileAcceptedSndCancelled u ft -> ttyUser u $ viewRcvFileSndCancelled ft
@@ -391,6 +390,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
CRArchiveImported archiveErrs -> if null archiveErrs then ["ok"] else ["archive import errors: " <> plain (show archiveErrs)]
CRAppSettings as -> ["app settings: " <> plain (LB.unpack $ J.encode as)]
CRTimedAction _ _ -> []
CRCustomChatResponse u r -> ttyUser' u $ [plain r]
where
ttyUser :: User -> [StyledString] -> [StyledString]
ttyUser user@User {showNtfs, activeUser} ss
@@ -1167,7 +1167,7 @@ viewNetworkConfig NetworkConfig {socksProxy, tcpTimeout} =
]
viewContactInfo :: Contact -> Maybe ConnectionStats -> Maybe Profile -> [StyledString]
viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink}, activeConn} stats incognitoProfile =
viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink}, activeConn, customData} stats incognitoProfile =
["contact ID: " <> sShow contactId]
<> maybe [] viewConnectionStats stats
<> maybe [] (\l -> ["contact address: " <> (plain . strEncode) (simplexChatContact l)]) contactLink
@@ -1179,12 +1179,17 @@ viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, conta
<> [viewConnectionVerified (contactSecurityCode ct)]
<> ["quantum resistant end-to-end encryption" | contactPQEnabled ct == CR.PQEncOn]
<> maybe [] (\ac -> [viewPeerChatVRange (peerChatVRange ac)]) activeConn
<> viewCustomData customData
viewGroupInfo :: GroupInfo -> GroupSummary -> [StyledString]
viewGroupInfo GroupInfo {groupId} s =
viewGroupInfo GroupInfo {groupId, customData} s =
[ "group ID: " <> sShow groupId,
"current members: " <> sShow (currentMembers s)
]
<> viewCustomData customData
viewCustomData :: Maybe CustomData -> [StyledString]
viewCustomData = maybe [] (\(CustomData v) -> ["custom data: " <> plain (LB.toStrict . J.encode $ J.Object v)])
viewGroupMemberInfo :: GroupInfo -> GroupMember -> Maybe ConnectionStats -> [StyledString]
viewGroupMemberInfo GroupInfo {groupId} m@GroupMember {groupMemberId, memberProfile = LocalProfile {localAlias, contactLink}, activeConn} stats =
+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"],