diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index d5ad40b850..81661c2fba 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -972,7 +972,7 @@ func apiGetGroupLink(_ groupId: Int64) throws -> (String, GroupMemberRole)? { func apiGetVersion() throws -> CoreVersionInfo { let r = chatSendCmdSync(.showVersion) - if case let .versionInfo(info) = r { return info } + if case let .versionInfo(info, _, _) = r { return info } throw r } @@ -983,10 +983,10 @@ private func currentUserId(_ funcName: String) throws -> Int64 { throw RuntimeError("\(funcName): no current user") } -func initializeChat(start: Bool, dbKey: String? = nil, refreshInvitations: Bool = true) throws { +func initializeChat(start: Bool, dbKey: String? = nil, refreshInvitations: Bool = true, confirmMigrations: MigrationConfirmation? = nil) throws { logger.debug("initializeChat") let m = ChatModel.shared - (m.chatDbEncrypted, m.chatDbStatus) = chatMigrateInit(dbKey) + (m.chatDbEncrypted, m.chatDbStatus) = chatMigrateInit(dbKey, confirmMigrations: confirmMigrations) if m.chatDbStatus != .ok { return } // If we migrated successfully means previous re-encryption process on database level finished successfully too if encryptionStartedDefault.get() { diff --git a/apps/ios/Shared/Views/Database/DatabaseErrorView.swift b/apps/ios/Shared/Views/Database/DatabaseErrorView.swift index 4830c81729..cd18b244c2 100644 --- a/apps/ios/Shared/Views/Database/DatabaseErrorView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseErrorView.swift @@ -16,40 +16,68 @@ struct DatabaseErrorView: View { @State private var storedDBKey = getDatabaseKey() @State private var useKeychain = storeDBPassphraseGroupDefault.get() @State private var showRestoreDbButton = false + @State private var starting = false var body: some View { + ZStack { + databaseErrorView().disabled(starting) + if starting { + ProgressView().scaleEffect(2) + } + } + } + + @ViewBuilder private func databaseErrorView() -> some View { VStack(alignment: .leading, spacing: 16) { switch status { case let .errorNotADatabase(dbFile): if useKeychain && storedDBKey != nil && storedDBKey != "" { - Text("Wrong database passphrase").font(.title) + titleText("Wrong database passphrase") Text("Database passphrase is different from saved in the keychain.") databaseKeyField(onSubmit: saveAndRunChat) saveAndOpenButton() - Text("File: \(dbFile)") + fileNameText(dbFile) } else { - Text("Encrypted database").font(.title) + titleText("Encrypted database") Text("Database passphrase is required to open chat.") if useKeychain { databaseKeyField(onSubmit: saveAndRunChat) saveAndOpenButton() } else { - databaseKeyField(onSubmit: runChat) + databaseKeyField(onSubmit: { runChat() }) openChatButton() } } - case let .error(dbFile, migrationError): - Text("Database error") - .font(.title) - Text("File: \(dbFile)") - Text("Error: \(migrationError)") + case let .errorMigration(dbFile, migrationError): + switch migrationError { + case let .upgrade(upMigrations): + titleText("Database upgrade") + Button("Upgrade and open chat") { runChat(confirmMigrations: .yesUp) } + fileNameText(dbFile) + migrationsText(upMigrations.map(\.upName)) + case let .downgrade(downMigrations): + titleText("Database downgrade") + Text("Warning: you may lose some data!").bold() + Button("Downgrade and open chat") { runChat(confirmMigrations: .yesUpDown) } + fileNameText(dbFile) + migrationsText(downMigrations) + case let .migrationError(mtrError): + titleText("Incompatible database version") + fileNameText(dbFile) + Text("Error: ") + Text(mtrErrorDescription(mtrError)) + } + case let .errorSQL(dbFile, migrationSQLError): + titleText("Database error") + fileNameText(dbFile) + Text("Error: \(migrationSQLError)") case .errorKeychain: - Text("Keychain error") - .font(.title) + titleText("Keychain error") Text("Cannot access keychain to save database password") + case .invalidConfirmation: + // this can only happen if incorrect parameter is passed + Text(String("Invalid migration confirmation")).font(.title) case let .unknown(json): - Text("Database error") - .font(.title) + titleText("Database error") Text("Unknown database error: \(json)") case .ok: EmptyView() @@ -61,10 +89,31 @@ struct DatabaseErrorView: View { } } .padding() - .frame(maxHeight: .infinity, alignment: .topLeading) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .onAppear() { showRestoreDbButton = shouldShowRestoreDbButton() } } + private func titleText(_ s: LocalizedStringKey) -> Text { + Text(s).font(.title) + } + + private func fileNameText(_ f: String) -> Text { + Text("File: \((f as NSString).lastPathComponent)") + } + + private func migrationsText(_ ms: [String]) -> Text { + Text("Migrations: \(ms.joined(separator: ", "))") + } + + private func mtrErrorDescription(_ err: MTRError) -> LocalizedStringKey { + switch err { + case let .noDown(dbMigrations): + return "database version is newer than the app, but no down migration for: \(dbMigrations.joined(separator: ", "))" + case let .different(appMigration, dbMigration): + return "different migration in the app/database: \(appMigration) / \(dbMigration)" + } + } + private func databaseKeyField(onSubmit: @escaping () -> Void) -> some View { PassphraseField(key: $dbKey, placeholder: "Enter passphrase…", valid: validKey(dbKey), onSubmit: onSubmit) } @@ -89,14 +138,24 @@ struct DatabaseErrorView: View { runChat() } - private func runChat() { + private func runChat(confirmMigrations: MigrationConfirmation? = nil) { + starting = true + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + runChatSync(confirmMigrations: confirmMigrations) + starting = false + } + } + + private func runChatSync(confirmMigrations: MigrationConfirmation? = nil) { do { resetChatCtrl() - try initializeChat(start: m.v3DBMigration.startChat, dbKey: dbKey) + try initializeChat(start: m.v3DBMigration.startChat, dbKey: useKeychain ? nil : dbKey, confirmMigrations: confirmMigrations) if let s = m.chatDbStatus { status = s let am = AlertManager.shared switch s { + case .invalidConfirmation: + am.showAlert(Alert(title: Text(String("Invalid migration confirmation")))) case .errorNotADatabase: am.showAlertMsg( title: "Wrong passphrase!", @@ -104,7 +163,7 @@ struct DatabaseErrorView: View { ) case .errorKeychain: am.showAlertMsg(title: "Keychain error") - case let .error(_, error): + case let .errorSQL(_, error): am.showAlert(Alert( title: Text("Database error"), message: Text(error) @@ -114,6 +173,7 @@ struct DatabaseErrorView: View { title: Text("Unknown error"), message: Text(error) )) + case .errorMigration: () case .ok: () } } diff --git a/apps/ios/Shared/Views/UserSettings/CallSettings.swift b/apps/ios/Shared/Views/UserSettings/CallSettings.swift index ca43faab03..43c715523e 100644 --- a/apps/ios/Shared/Views/UserSettings/CallSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/CallSettings.swift @@ -11,7 +11,7 @@ import SimpleXChat struct CallSettings: View { @AppStorage(DEFAULT_WEBRTC_POLICY_RELAY) private var webrtcPolicyRelay = true - @AppStorage(GROUP_DEFAULT_CALL_KIT_ENABLED, store: UserDefaults(suiteName: APP_GROUP_NAME)!) private var callKitEnabled = true + @AppStorage(GROUP_DEFAULT_CALL_KIT_ENABLED, store: groupDefaults) private var callKitEnabled = true @AppStorage(DEFAULT_CALL_KIT_CALLS_IN_RECENTS) private var callKitCallsInRecents = false @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false private let allowChangingCallsHistory = false diff --git a/apps/ios/Shared/Views/UserSettings/DeveloperView.swift b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift new file mode 100644 index 0000000000..0d7435d909 --- /dev/null +++ b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift @@ -0,0 +1,50 @@ +// +// DeveloperView.swift +// SimpleX (iOS) +// +// Created by Evgeny on 26/03/2023. +// Copyright © 2023 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct DeveloperView: View { + @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false + @AppStorage(GROUP_DEFAULT_CONFIRM_DB_UPGRADES, store: groupDefaults) private var confirmDatabaseUpgrades = false + @Environment(\.colorScheme) var colorScheme + + var body: some View { + VStack { + List { + Section { + NavigationLink { + TerminalView() + } label: { + settingsRow("terminal") { Text("Chat console") } + } + settingsRow("chevron.left.forwardslash.chevron.right") { + Toggle("Show developer options", isOn: $developerTools) + } + settingsRow("internaldrive") { + Toggle("Confirm database upgrades", isOn: $confirmDatabaseUpgrades) + } + ZStack(alignment: .leading) { + Image(colorScheme == .dark ? "github_light" : "github") + .resizable() + .frame(width: 24, height: 24) + .opacity(0.5) + Text("Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)") + .padding(.leading, 36) + } + } + } + } + } +} + +struct DeveloperView_Previews: PreviewProvider { + static var previews: some View { + DeveloperView() + } +} diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index cb58d2feaf..e885fc1f2d 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -108,7 +108,6 @@ struct SettingsView: View { @EnvironmentObject var chatModel: ChatModel @EnvironmentObject var sceneDelegate: SceneDelegate @Binding var showSettings: Bool - @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @State private var settingsSheet: SettingsSheet? var body: some View { @@ -259,23 +258,11 @@ struct SettingsView: View { } Section("Develop") { - settingsRow("chevron.left.forwardslash.chevron.right") { - Toggle("Developer tools", isOn: $developerTools) - } - if developerTools { - NavigationLink { - TerminalView() - } label: { - settingsRow("terminal") { Text("Chat console") } - } - ZStack(alignment: .leading) { - Image(colorScheme == .dark ? "github_light" : "github") - .resizable() - .frame(width: 24, height: 24) - .opacity(0.5) - Text("Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)") - .padding(.leading, indent) - } + NavigationLink { + DeveloperView() + .navigationTitle("Developer tools") + } label: { + settingsRow("chevron.left.forwardslash.chevron.right") { Text("Developer tools") } } // NavigationLink { // ExperimentalFeaturesView() diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index 5eda201f24..2b511f7b4c 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -203,7 +203,7 @@ var networkConfig: NetCfg = getNetCfg() func startChat() -> DBMigrationResult? { hs_init(0, nil) if chatStarted { return .ok } - let (_, dbStatus) = chatMigrateInit() + let (_, dbStatus) = chatMigrateInit(confirmMigrations: defaultMigrationConfirmation()) if dbStatus != .ok { resetChatCtrl() return dbStatus diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 053a59b9fe..08b902463b 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -58,6 +58,7 @@ 5C65DAF529CBA429003CEE45 /* libHSsimplex-chat-4.6.0.0-KxI2qGrpKDHEZQGy0eoUXU.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C65DAF029CBA429003CEE45 /* libHSsimplex-chat-4.6.0.0-KxI2qGrpKDHEZQGy0eoUXU.a */; }; 5C65DAF629CBA429003CEE45 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C65DAF129CBA429003CEE45 /* libgmpxx.a */; }; 5C65DAF729CBA429003CEE45 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C65DAF229CBA429003CEE45 /* libffi.a */; }; + 5C65DAF929D0CC20003CEE45 /* DeveloperView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C65DAF829D0CC20003CEE45 /* DeveloperView.swift */; }; 5C65F343297D45E100B67AF3 /* VersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C65F341297D3F3600B67AF3 /* VersionView.swift */; }; 5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6AD81227A834E300348BD7 /* NewChatButton.swift */; }; 5C6BA667289BD954009B8ECC /* DismissSheets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6BA666289BD954009B8ECC /* DismissSheets.swift */; }; @@ -294,6 +295,7 @@ 5C65DAF029CBA429003CEE45 /* libHSsimplex-chat-4.6.0.0-KxI2qGrpKDHEZQGy0eoUXU.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.6.0.0-KxI2qGrpKDHEZQGy0eoUXU.a"; sourceTree = ""; }; 5C65DAF129CBA429003CEE45 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 5C65DAF229CBA429003CEE45 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + 5C65DAF829D0CC20003CEE45 /* DeveloperView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperView.swift; sourceTree = ""; }; 5C65F341297D3F3600B67AF3 /* VersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionView.swift; sourceTree = ""; }; 5C6AD81227A834E300348BD7 /* NewChatButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewChatButton.swift; sourceTree = ""; }; 5C6BA666289BD954009B8ECC /* DismissSheets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissSheets.swift; sourceTree = ""; }; @@ -691,6 +693,7 @@ 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */, 18415845648CA4F5A8BCA272 /* UserProfilesView.swift */, 5C65F341297D3F3600B67AF3 /* VersionView.swift */, + 5C65DAF829D0CC20003CEE45 /* DeveloperView.swift */, ); path = UserSettings; sourceTree = ""; @@ -1022,6 +1025,7 @@ 5C93292F29239A170090FFF9 /* SMPServersView.swift in Sources */, 5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */, 5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */, + 5C65DAF929D0CC20003CEE45 /* DeveloperView.swift in Sources */, 5C36027327F47AD5009F19D9 /* AppDelegate.swift in Sources */, 5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */, 5CB0BA9A2827FD8800B3292C /* HowItWorks.swift in Sources */, diff --git a/apps/ios/SimpleXChat/API.swift b/apps/ios/SimpleXChat/API.swift index 11eec7f873..a400a82fb4 100644 --- a/apps/ios/SimpleXChat/API.swift +++ b/apps/ios/SimpleXChat/API.swift @@ -17,7 +17,7 @@ public func getChatCtrl(_ useKey: String? = nil) -> chat_ctrl { fatalError("chat controller not initialized") } -public func chatMigrateInit(_ useKey: String? = nil) -> (Bool, DBMigrationResult) { +public func chatMigrateInit(_ useKey: String? = nil, confirmMigrations: MigrationConfirmation? = nil) -> (Bool, DBMigrationResult) { if let res = migrationResult { return res } let dbPath = getAppDatabasePath().path var dbKey = "" @@ -34,12 +34,14 @@ public func chatMigrateInit(_ useKey: String? = nil) -> (Bool, DBMigrationResult dbKey = key } } - logger.debug("chatMigrateInit DB path: \(dbPath)") + let confirm = confirmMigrations ?? defaultMigrationConfirmation() + logger.debug("chatMigrateInit DB path: \(dbPath), confirm: \(confirm.rawValue)") // logger.debug("chatMigrateInit DB key: \(dbKey)") var cPath = dbPath.cString(using: .utf8)! var cKey = dbKey.cString(using: .utf8)! + var cConfirm = confirm.rawValue.cString(using: .utf8)! // the last parameter of chat_migrate_init is used to return the pointer to chat controller - let cjson = chat_migrate_init(&cPath, &cKey, &chatController)! + let cjson = chat_migrate_init(&cPath, &cKey, &cConfirm, &chatController)! let dbRes = dbMigrationResult(fromCString(cjson)) let encrypted = dbKey != "" let keychainErr = dbRes == .ok && useKeychain && encrypted && !setDatabaseKey(dbKey) @@ -207,12 +209,40 @@ func chatErrorString(_ err: ChatError) -> String { public enum DBMigrationResult: Decodable, Equatable { case ok + case invalidConfirmation case errorNotADatabase(dbFile: String) - case error(dbFile: String, migrationError: String) + case errorMigration(dbFile: String, migrationError: MigrationError) + case errorSQL(dbFile: String, migrationSQLError: String) case errorKeychain case unknown(json: String) } +public enum MigrationConfirmation: String { + case yesUp + case yesUpDown + case error +} + +public func defaultMigrationConfirmation() -> MigrationConfirmation { + confirmDBUpgradesGroupDefault.get() ? .error : .yesUp +} + +public enum MigrationError: Decodable, Equatable { + case upgrade(upMigrations: [UpMigration]) + case downgrade(downMigrations: [String]) + case migrationError(mtrError: MTRError) +} + +public struct UpMigration: Decodable, Equatable { + public var upName: String +// public var withDown: Bool +} + +public enum MTRError: Decodable, Equatable { + case noDown(dbMigrations: [String]) + case different(appMigration: String, dbMigration: String) +} + func dbMigrationResult(_ s: String) -> DBMigrationResult { let d = s.data(using: .utf8)! // TODO is there a way to do it without copying the data? e.g: diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index ededd5b26b..d57d1b939d 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -459,7 +459,7 @@ public enum ChatResponse: Decodable, Error { case ntfMessages(user_: User?, connEntity: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo]) case newContactConnection(user: User, connection: PendingContactConnection) case contactConnectionDeleted(user: User, connection: PendingContactConnection) - case versionInfo(versionInfo: CoreVersionInfo) + case versionInfo(versionInfo: CoreVersionInfo, chatMigrations: [UpMigration], agentMigrations: [UpMigration]) case cmdOk(user: User?) case chatCmdError(user_: User?, chatError: ChatError) case chatError(user_: User?, chatError: ChatError) @@ -674,7 +674,7 @@ public enum ChatResponse: Decodable, Error { case let .ntfMessages(u, connEntity, msgTs, ntfMessages): return withUser(u, "connEntity: \(String(describing: connEntity))\nmsgTs: \(String(describing: msgTs))\nntfMessages: \(String(describing: ntfMessages))") case let .newContactConnection(u, connection): return withUser(u, String(describing: connection)) case let .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection)) - case let .versionInfo(versionInfo): return String(describing: versionInfo) + case let .versionInfo(versionInfo, chatMigrations, agentMigrations): return "\(String(describing: versionInfo))\n\nchat migrations: \(chatMigrations.map(\.upName))\n\nagent migrations: \(agentMigrations.map(\.upName))" case .cmdOk: return noDetails case let .chatCmdError(u, chatError): return withUser(u, String(describing: chatError)) case let .chatError(u, chatError): return withUser(u, String(describing: chatError)) diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index 3ea392c229..c93a3acbc6 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -29,6 +29,7 @@ let GROUP_DEFAULT_NETWORK_TCP_KEEP_CNT = "networkTCPKeepCnt" let GROUP_DEFAULT_INCOGNITO = "incognito" let GROUP_DEFAULT_STORE_DB_PASSPHRASE = "storeDBPassphrase" let GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE = "initialRandomDBPassphrase" +public let GROUP_DEFAULT_CONFIRM_DB_UPGRADES = "confirmDBUpgrades" public let GROUP_DEFAULT_CALL_KIT_ENABLED = "callKitEnabled" public let APP_GROUP_NAME = "group.chat.simplex.app" @@ -52,6 +53,7 @@ public func registerGroupDefaults() { GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE: false, GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES: true, GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE: false, + GROUP_DEFAULT_CONFIRM_DB_UPGRADES: false, GROUP_DEFAULT_CALL_KIT_ENABLED: true ]) } @@ -121,6 +123,8 @@ public let storeDBPassphraseGroupDefault = BoolDefault(defaults: groupDefaults, public let initialRandomDBPassphraseGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE) +public let confirmDBUpgradesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_CONFIRM_DB_UPGRADES) + public let callKitEnabledGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_CALL_KIT_ENABLED) public class DateDefault { diff --git a/apps/ios/SimpleXChat/Notifications.swift b/apps/ios/SimpleXChat/Notifications.swift index 035e849ff5..2e8c5c7124 100644 --- a/apps/ios/SimpleXChat/Notifications.swift +++ b/apps/ios/SimpleXChat/Notifications.swift @@ -127,12 +127,16 @@ public func createErrorNtf(_ dbStatus: DBMigrationResult) -> UNMutableNotificati switch dbStatus { case .errorNotADatabase: title = NSLocalizedString("Encrypted message: no passphrase", comment: "notification") - case .error: + case .errorMigration: + title = NSLocalizedString("Encrypted message: database migration error", comment: "notification") + case .errorSQL: title = NSLocalizedString("Encrypted message: database error", comment: "notification") case .errorKeychain: title = NSLocalizedString("Encrypted message: keychain error", comment: "notification") case .unknown: title = NSLocalizedString("Encrypted message: unexpected error", comment: "notification") + case .invalidConfirmation: + title = NSLocalizedString("Encrypted message or another event", comment: "notification") case .ok: title = NSLocalizedString("Encrypted message or another event", comment: "notification") } diff --git a/apps/ios/SimpleXChat/SimpleX.h b/apps/ios/SimpleXChat/SimpleX.h index 5d5f1e355a..199c688f26 100644 --- a/apps/ios/SimpleXChat/SimpleX.h +++ b/apps/ios/SimpleXChat/SimpleX.h @@ -16,7 +16,7 @@ extern void hs_init(int argc, char **argv[]); typedef void* chat_ctrl; // the last parameter is used to return the pointer to chat controller -extern char *chat_migrate_init(char *path, char *key, chat_ctrl *ctrl); +extern char *chat_migrate_init(char *path, char *key, char *confirm, chat_ctrl *ctrl); extern char *chat_send_cmd(chat_ctrl ctl, char *cmd); extern char *chat_recv_msg(chat_ctrl ctl); extern char *chat_recv_msg_wait(chat_ctrl ctl, int wait); diff --git a/cabal.project b/cabal.project index 22c7397220..cc143cc0fb 100644 --- a/cabal.project +++ b/cabal.project @@ -7,7 +7,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: d41c2bec2af2aa77e7d671800c08c9760187dff9 + tag: 6a665a083387fe7145d161957f0fcab223a48838 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 87c6dd1362..9ffd79f722 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."d41c2bec2af2aa77e7d671800c08c9760187dff9" = "1lrbxbggpa4cq190gwk1bljx7y8fhfbpk4wnsdy67fpk32mk7bc1"; + "https://github.com/simplex-chat/simplexmq.git"."6a665a083387fe7145d161957f0fcab223a48838" = "06nmqbnvalwx8zc8dndzcp31asm65clx519aplzpkipjcbyz93y4"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/kazu-yamamoto/http2.git"."78e18f52295a7f89e828539a03fbcb24931461a3" = "05q165anvv0qrcxqbvq1dlvw0l8gmsa9kl6sazk1mfhz2g0yimdk"; "https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd"; diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index a5b2f1bab3..fc3fe78783 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -61,10 +61,11 @@ import Simplex.Chat.Util (diffInMicros, diffInSeconds) import Simplex.FileTransfer.Client.Presets (defaultXFTPServers) import Simplex.Messaging.Agent as Agent import Simplex.Messaging.Agent.Client (AgentStatsKey (..)) -import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), AgentDatabase (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig) +import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig) import Simplex.Messaging.Agent.Lock import Simplex.Messaging.Agent.Protocol -import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (dbNew), execSQL) +import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, SQLiteStore (dbNew), execSQL, upMigration) +import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations import Simplex.Messaging.Client (defaultNetworkConfig) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding @@ -92,11 +93,9 @@ defaultChatConfig = { agentConfig = defaultAgentConfig { tcpPort = undefined, -- agent does not listen to TCP - tbqSize = 1024, - database = AgentDBFile {dbFile = "simplex_v1_agent", dbKey = ""}, - yesToMigrations = False + tbqSize = 1024 }, - yesToMigrations = False, + confirmMigrations = MCConsole, defaultServers = DefaultAgentServers { smp = _defaultSMPServers, @@ -134,10 +133,10 @@ fixedImagePreview = ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEA logCfg :: LogConfig logCfg = LogConfig {lc_file = Nothing, lc_stderr = True} -createChatDatabase :: FilePath -> String -> Bool -> IO ChatDatabase -createChatDatabase filePrefix key yesToMigrations = do - chatStore <- createChatStore (chatStoreFile filePrefix) key yesToMigrations - agentStore <- createAgentStore (agentStoreFile filePrefix) key yesToMigrations +createChatDatabase :: FilePath -> String -> MigrationConfirmation -> IO (Either MigrationError ChatDatabase) +createChatDatabase filePrefix key confirmMigrations = runExceptT $ do + chatStore <- ExceptT $ createChatStore (chatStoreFile filePrefix) key confirmMigrations + agentStore <- ExceptT $ createAgentStore (agentStoreFile filePrefix) key confirmMigrations pure ChatDatabase {chatStore, agentStore} newChatController :: ChatDatabase -> Maybe User -> ChatConfig -> ChatOpts -> Maybe (Notification -> IO ()) -> IO ChatController @@ -148,9 +147,10 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen firstTime = dbNew chatStore activeTo <- newTVarIO ActiveNone currentUser <- newTVarIO user - smpAgent <- getSMPAgentClient aCfg {tbqSize, database = AgentDB agentStore} =<< agentServers config + servers <- agentServers config + smpAgent <- getSMPAgentClient aCfg {tbqSize} servers agentStore agentAsync <- newTVarIO Nothing - idsDrg <- newTVarIO =<< drgNew + idsDrg <- newTVarIO =<< liftIO drgNew inputQ <- newTBQueueIO tbqSize outputQ <- newTBQueueIO tbqSize notifyQ <- newTBQueueIO tbqSize @@ -1389,7 +1389,11 @@ processChatCommand = \case updateGroupProfileByName gName $ \p -> p {groupPreferences = Just . setGroupPreference' SGFTimedMessages pref $ groupPreferences p} QuitChat -> liftIO exitSuccess - ShowVersion -> pure $ CRVersionInfo $ coreVersionInfo $(buildTimestampQ) $(simplexmqCommitQ) + ShowVersion -> do + let versionInfo = coreVersionInfo $(buildTimestampQ) $(simplexmqCommitQ) + chatMigrations <- map upMigration <$> withStore' Migrations.getCurrent + agentMigrations <- withAgent getAgentMigrations + pure $ CRVersionInfo {versionInfo, chatMigrations, agentMigrations} DebugLocks -> do chatLockName <- atomically . tryReadTMVar =<< asks chatLock agentLocks <- withAgent debugAgentLocks diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 6bb8169dbd..5c1379011a 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -49,7 +49,7 @@ import Simplex.Messaging.Agent.Client (AgentLocks, SMPTestFailure) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig) import Simplex.Messaging.Agent.Lock import Simplex.Messaging.Agent.Protocol -import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore) +import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation, SQLiteStore, UpMigration) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus) @@ -101,7 +101,7 @@ coreVersionInfo buildTimestamp simplexmqCommit = data ChatConfig = ChatConfig { agentConfig :: AgentConfig, - yesToMigrations :: Bool, + confirmMigrations :: MigrationConfirmation, defaultServers :: DefaultAgentServers, tbqSize :: Natural, fileChunkSize :: Integer, @@ -415,7 +415,7 @@ data ChatResponse | CRUserProfile {user :: User, profile :: Profile} | CRUserProfileNoChange {user :: User} | CRUserPrivacy {user :: User} - | CRVersionInfo {versionInfo :: CoreVersionInfo} + | CRVersionInfo {versionInfo :: CoreVersionInfo, chatMigrations :: [UpMigration], agentMigrations :: [UpMigration]} | CRInvitation {user :: User, connReqInvitation :: ConnReqInvitation} | CRSentConfirmation {user :: User} | CRSentInvitation {user :: User, customUserProfile :: Maybe Profile} diff --git a/src/Simplex/Chat/Core.hs b/src/Simplex/Chat/Core.hs index aa242a0899..a62ab5642b 100644 --- a/src/Simplex/Chat/Core.hs +++ b/src/Simplex/Chat/Core.hs @@ -11,18 +11,22 @@ import Simplex.Chat import Simplex.Chat.Controller import Simplex.Chat.Options (ChatOpts (..), CoreChatOpts (..)) import Simplex.Chat.Types +import System.Exit (exitFailure) import UnliftIO.Async simplexChatCore :: ChatConfig -> ChatOpts -> Maybe (Notification -> IO ()) -> (User -> ChatController -> IO ()) -> IO () -simplexChatCore cfg@ChatConfig {yesToMigrations} opts@ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, dbKey, logAgent}} sendToast chat = +simplexChatCore cfg@ChatConfig {confirmMigrations} opts@ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, dbKey, logAgent}} sendToast chat = case logAgent of Just level -> do setLogLevel level withGlobalLogging logCfg initRun _ -> initRun where - initRun = do - db@ChatDatabase {chatStore} <- createChatDatabase dbFilePrefix dbKey yesToMigrations + initRun = createChatDatabase dbFilePrefix dbKey confirmMigrations >>= either exit run + exit e = do + putStrLn $ "Error opening database: " <> show e + exitFailure + run db@ChatDatabase {chatStore} = do u <- getCreateActiveUser chatStore cc <- newChatController db (Just u) cfg opts sendToast runSimplexChat opts u cc chat diff --git a/src/Simplex/Chat/Migrations/M20230317_hidden_profiles.hs b/src/Simplex/Chat/Migrations/M20230317_hidden_profiles.hs index 27ae711a04..65e9cfeadd 100644 --- a/src/Simplex/Chat/Migrations/M20230317_hidden_profiles.hs +++ b/src/Simplex/Chat/Migrations/M20230317_hidden_profiles.hs @@ -12,3 +12,11 @@ ALTER TABLE users ADD COLUMN view_pwd_hash BLOB; ALTER TABLE users ADD COLUMN view_pwd_salt BLOB; ALTER TABLE users ADD COLUMN show_ntfs INTEGER NOT NULL DEFAULT 1; |] + +down_m20230317_hidden_profiles :: Query +down_m20230317_hidden_profiles = + [sql| +ALTER TABLE users DROP COLUMN view_pwd_hash; +ALTER TABLE users DROP COLUMN view_pwd_salt; +ALTER TABLE users DROP COLUMN show_ntfs; +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index 1cb392f0ea..7eccb9a1ba 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -1,6 +1,7 @@ CREATE TABLE migrations( name TEXT NOT NULL, ts TEXT NOT NULL, + down TEXT, PRIMARY KEY(name) ); CREATE TABLE contact_profiles( diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index bbd5a475e3..04493351ea 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -12,6 +12,7 @@ import Control.Monad.Except import Control.Monad.Reader import Data.Aeson (ToJSON (..)) import qualified Data.Aeson as J +import Data.Bifunctor (first) import qualified Data.ByteString.Base64.URL as U import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as LB @@ -37,26 +38,17 @@ import Simplex.Chat.Mobile.WebRTC import Simplex.Chat.Options import Simplex.Chat.Store import Simplex.Chat.Types -import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (yesToMigrations), createAgentStore) -import Simplex.Messaging.Agent.Store.SQLite (closeSQLiteStore) +import Simplex.Messaging.Agent.Env.SQLite (createAgentStore) +import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError) import Simplex.Messaging.Client (defaultNetworkConfig) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) import Simplex.Messaging.Protocol (BasicAuth (..), CorrId (..), ProtoServerWithAuth (..), ProtocolServer (..), SMPServerWithAuth) -import Simplex.Messaging.Util (catchAll, safeDecodeUtf8) +import Simplex.Messaging.Util (catchAll, liftEitherWith, safeDecodeUtf8) import System.Timeout (timeout) -foreign export ccall "chat_migrate_init" cChatMigrateInit :: CString -> CString -> Ptr (StablePtr ChatController) -> IO CJSONString - --- TODO remove -foreign export ccall "chat_migrate_db" cChatMigrateDB :: CString -> CString -> IO CJSONString - --- chat_init is deprecated -foreign export ccall "chat_init" cChatInit :: CString -> IO (StablePtr ChatController) - --- TODO remove -foreign export ccall "chat_init_key" cChatInitKey :: CString -> CString -> IO (StablePtr ChatController) +foreign export ccall "chat_migrate_init" cChatMigrateInit :: CString -> CString -> CString -> Ptr (StablePtr ChatController) -> IO CJSONString foreign export ccall "chat_send_cmd" cChatSendCmd :: StablePtr ChatController -> CString -> IO CJSONString @@ -75,35 +67,17 @@ foreign export ccall "chat_encrypt_media" cChatEncryptMedia :: CString -> Ptr Wo foreign export ccall "chat_decrypt_media" cChatDecryptMedia :: CString -> Ptr Word8 -> CInt -> IO CString -- | check / migrate database and initialize chat controller on success -cChatMigrateInit :: CString -> CString -> Ptr (StablePtr ChatController) -> IO CJSONString -cChatMigrateInit fp key ctrl = do +cChatMigrateInit :: CString -> CString -> CString -> Ptr (StablePtr ChatController) -> IO CJSONString +cChatMigrateInit fp key conf ctrl = do dbPath <- peekCAString fp dbKey <- peekCAString key + confirm <- peekCAString conf r <- - chatMigrateInit dbPath dbKey >>= \case + chatMigrateInit dbPath dbKey confirm >>= \case Right cc -> (newStablePtr cc >>= poke ctrl) $> DBMOk Left e -> pure e newCAString . LB.unpack $ J.encode r --- | check and migrate the database --- This function validates that the encryption is correct and runs migrations - it should be called before cChatInitKey --- TODO remove -cChatMigrateDB :: CString -> CString -> IO CJSONString -cChatMigrateDB fp key = - ((,) <$> peekCAString fp <*> peekCAString key) >>= uncurry chatMigrateDB >>= newCAString . LB.unpack . J.encode - --- | initialize chat controller (deprecated) --- The active user has to be created and the chat has to be started before most commands can be used. -cChatInit :: CString -> IO (StablePtr ChatController) -cChatInit fp = peekCAString fp >>= chatInit >>= newStablePtr - --- | initialize chat controller with encrypted database --- The active user has to be created and the chat has to be started before most commands can be used. --- TODO remove -cChatInitKey :: CString -> CString -> IO (StablePtr ChatController) -cChatInitKey fp key = - ((,) <$> peekCAString fp <*> peekCAString key) >>= uncurry chatInitKey >>= newStablePtr - -- | send command to chat (same syntax as in terminal for now) cChatSendCmd :: StablePtr ChatController -> CString -> IO CJSONString cChatSendCmd cPtr cCmd = do @@ -159,10 +133,7 @@ mobileChatOpts dbFilePrefix dbKey = defaultMobileConfig :: ChatConfig defaultMobileConfig = - defaultChatConfig - { yesToMigrations = True, - agentConfig = (agentConfig defaultChatConfig) {yesToMigrations = True} - } + defaultChatConfig {confirmMigrations = MCYesUp} type CJSONString = CString @@ -171,60 +142,36 @@ getActiveUser_ st = find activeUser <$> withTransaction st getUsers data DBMigrationResult = DBMOk + | DBMInvalidConfirmation | DBMErrorNotADatabase {dbFile :: String} - | DBMError {dbFile :: String, migrationError :: String} + | DBMErrorMigration {dbFile :: String, migrationError :: MigrationError} + | DBMErrorSQL {dbFile :: String, migrationSQLError :: String} deriving (Show, Generic) instance ToJSON DBMigrationResult where toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "DBM" toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "DBM" -chatMigrateInit :: String -> String -> IO (Either DBMigrationResult ChatController) -chatMigrateInit dbFilePrefix dbKey = runExceptT $ do - chatStore <- migrate createChatStore $ chatStoreFile dbFilePrefix - agentStore <- migrate createAgentStore $ agentStoreFile dbFilePrefix +chatMigrateInit :: String -> String -> String -> IO (Either DBMigrationResult ChatController) +chatMigrateInit dbFilePrefix dbKey confirm = runExceptT $ do + 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} where initialize st db = do user_ <- getActiveUser_ st newChatController db user_ defaultMobileConfig (mobileChatOpts dbFilePrefix dbKey) Nothing - migrate createStore dbFile = + migrate createStore dbFile confirmMigrations = ExceptT $ - (Right <$> createStore dbFile dbKey True) + (first (DBMErrorMigration dbFile) <$> createStore dbFile dbKey confirmMigrations) `catch` (pure . checkDBError) `catchAll` (pure . dbError) where checkDBError e = case sqlError e of DB.ErrorNotADatabase -> Left $ DBMErrorNotADatabase dbFile _ -> dbError e - dbError e = Left . DBMError dbFile $ show e - --- TODO remove -chatMigrateDB :: String -> String -> IO DBMigrationResult -chatMigrateDB dbFilePrefix dbKey = - migrate createChatStore (chatStoreFile dbFilePrefix) >>= \case - DBMOk -> migrate createAgentStore (agentStoreFile dbFilePrefix) - e -> pure e - where - migrate createStore dbFile = - ((createStore dbFile dbKey True >>= closeSQLiteStore) $> DBMOk) - `catch` (pure . checkDBError) - `catchAll` (pure . dbError) - where - checkDBError e = case sqlError e of - DB.ErrorNotADatabase -> DBMErrorNotADatabase dbFile - _ -> dbError e - dbError e = DBMError dbFile $ show e - -chatInit :: String -> IO ChatController -chatInit = (`chatInitKey` "") - --- TODO remove -chatInitKey :: String -> String -> IO ChatController -chatInitKey dbFilePrefix dbKey = do - db@ChatDatabase {chatStore} <- createChatDatabase dbFilePrefix dbKey True - user_ <- getActiveUser_ chatStore - newChatController db user_ defaultMobileConfig (mobileChatOpts dbFilePrefix dbKey) Nothing + dbError e = Left . DBMErrorSQL dbFile $ show e chatSendCmd :: ChatController -> String -> IO JSONString chatSendCmd cc s = LB.unpack . J.encode . APIResponse Nothing <$> runReaderT (execChatCommand $ B.pack s) cc diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 56c8faf7bc..fef0fd90c4 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -23,6 +23,7 @@ module Simplex.Chat.Store UserContactLink (..), AutoAccept (..), createChatStore, + migrations, -- used in tests chatStoreFile, agentStoreFile, createUserRecord, @@ -273,10 +274,9 @@ import Data.Bifunctor (first) import qualified Data.ByteString.Base64 as B64 import Data.ByteString.Char8 (ByteString) import Data.Either (rights) -import Data.Function (on) import Data.Functor (($>)) import Data.Int (Int64) -import Data.List (sortBy, sortOn) +import Data.List (sortOn) import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as L import Data.Maybe (fromMaybe, isJust, isNothing, listToMaybe, mapMaybe) @@ -353,7 +353,7 @@ import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Chat.Util (week) import Simplex.Messaging.Agent.Protocol (ACorrId, AgentMsgId, ConnId, InvitationId, MsgMeta (..), UserId) -import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), createSQLiteStore, firstRow, firstRow', maybeFirstRow, withTransaction) +import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation, MigrationError, SQLiteStore (..), createSQLiteStore, firstRow, firstRow', maybeFirstRow, withTransaction) import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) @@ -362,71 +362,71 @@ import Simplex.Messaging.Transport.Client (TransportHost) import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8) import UnliftIO.STM -schemaMigrations :: [(String, Query)] +schemaMigrations :: [(String, Query, Maybe Query)] schemaMigrations = - [ ("20220101_initial", m20220101_initial), - ("20220122_v1_1", m20220122_v1_1), - ("20220205_chat_item_status", m20220205_chat_item_status), - ("20220210_deduplicate_contact_requests", m20220210_deduplicate_contact_requests), - ("20220224_messages_fks", m20220224_messages_fks), - ("20220301_smp_servers", m20220301_smp_servers), - ("20220302_profile_images", m20220302_profile_images), - ("20220304_msg_quotes", m20220304_msg_quotes), - ("20220321_chat_item_edited", m20220321_chat_item_edited), - ("20220404_files_status_fields", m20220404_files_status_fields), - ("20220514_profiles_user_id", m20220514_profiles_user_id), - ("20220626_auto_reply", m20220626_auto_reply), - ("20220702_calls", m20220702_calls), - ("20220715_groups_chat_item_id", m20220715_groups_chat_item_id), - ("20220811_chat_items_indices", m20220811_chat_items_indices), - ("20220812_incognito_profiles", m20220812_incognito_profiles), - ("20220818_chat_notifications", m20220818_chat_notifications), - ("20220822_groups_host_conn_custom_user_profile_id", m20220822_groups_host_conn_custom_user_profile_id), - ("20220823_delete_broken_group_event_chat_items", m20220823_delete_broken_group_event_chat_items), - ("20220824_profiles_local_alias", m20220824_profiles_local_alias), - ("20220909_commands", m20220909_commands), - ("20220926_connection_alias", m20220926_connection_alias), - ("20220928_settings", m20220928_settings), - ("20221001_shared_msg_id_indices", m20221001_shared_msg_id_indices), - ("20221003_delete_broken_integrity_error_chat_items", m20221003_delete_broken_integrity_error_chat_items), - ("20221004_idx_msg_deliveries_message_id", m20221004_idx_msg_deliveries_message_id), - ("20221011_user_contact_links_group_id", m20221011_user_contact_links_group_id), - ("20221012_inline_files", m20221012_inline_files), - ("20221019_unread_chat", m20221019_unread_chat), - ("20221021_auto_accept__group_links", m20221021_auto_accept__group_links), - ("20221024_contact_used", m20221024_contact_used), - ("20221025_chat_settings", m20221025_chat_settings), - ("20221029_group_link_id", m20221029_group_link_id), - ("20221112_server_password", m20221112_server_password), - ("20221115_server_cfg", m20221115_server_cfg), - ("20221129_delete_group_feature_items", m20221129_delete_group_feature_items), - ("20221130_delete_item_deleted", m20221130_delete_item_deleted), - ("20221209_verified_connection", m20221209_verified_connection), - ("20221210_idxs", m20221210_idxs), - ("20221211_group_description", m20221211_group_description), - ("20221212_chat_items_timed", m20221212_chat_items_timed), - ("20221214_live_message", m20221214_live_message), - ("20221222_chat_ts", m20221222_chat_ts), - ("20221223_idx_chat_items_item_status", m20221223_idx_chat_items_item_status), - ("20221230_idxs", m20221230_idxs), - ("20230107_connections_auth_err_counter", m20230107_connections_auth_err_counter), - ("20230111_users_agent_user_id", m20230111_users_agent_user_id), - ("20230117_fkey_indexes", m20230117_fkey_indexes), - ("20230118_recreate_smp_servers", m20230118_recreate_smp_servers), - ("20230129_drop_chat_items_group_idx", m20230129_drop_chat_items_group_idx), - ("20230206_item_deleted_by_group_member_id", m20230206_item_deleted_by_group_member_id), - ("20230303_group_link_role", m20230303_group_link_role), - ("20230317_hidden_profiles", m20230317_hidden_profiles) + [ ("20220101_initial", m20220101_initial, Nothing), + ("20220122_v1_1", m20220122_v1_1, Nothing), + ("20220205_chat_item_status", m20220205_chat_item_status, Nothing), + ("20220210_deduplicate_contact_requests", m20220210_deduplicate_contact_requests, Nothing), + ("20220224_messages_fks", m20220224_messages_fks, Nothing), + ("20220301_smp_servers", m20220301_smp_servers, Nothing), + ("20220302_profile_images", m20220302_profile_images, Nothing), + ("20220304_msg_quotes", m20220304_msg_quotes, Nothing), + ("20220321_chat_item_edited", m20220321_chat_item_edited, Nothing), + ("20220404_files_status_fields", m20220404_files_status_fields, Nothing), + ("20220514_profiles_user_id", m20220514_profiles_user_id, Nothing), + ("20220626_auto_reply", m20220626_auto_reply, Nothing), + ("20220702_calls", m20220702_calls, Nothing), + ("20220715_groups_chat_item_id", m20220715_groups_chat_item_id, Nothing), + ("20220811_chat_items_indices", m20220811_chat_items_indices, Nothing), + ("20220812_incognito_profiles", m20220812_incognito_profiles, Nothing), + ("20220818_chat_notifications", m20220818_chat_notifications, Nothing), + ("20220822_groups_host_conn_custom_user_profile_id", m20220822_groups_host_conn_custom_user_profile_id, Nothing), + ("20220823_delete_broken_group_event_chat_items", m20220823_delete_broken_group_event_chat_items, Nothing), + ("20220824_profiles_local_alias", m20220824_profiles_local_alias, Nothing), + ("20220909_commands", m20220909_commands, Nothing), + ("20220926_connection_alias", m20220926_connection_alias, Nothing), + ("20220928_settings", m20220928_settings, Nothing), + ("20221001_shared_msg_id_indices", m20221001_shared_msg_id_indices, Nothing), + ("20221003_delete_broken_integrity_error_chat_items", m20221003_delete_broken_integrity_error_chat_items, Nothing), + ("20221004_idx_msg_deliveries_message_id", m20221004_idx_msg_deliveries_message_id, Nothing), + ("20221011_user_contact_links_group_id", m20221011_user_contact_links_group_id, Nothing), + ("20221012_inline_files", m20221012_inline_files, Nothing), + ("20221019_unread_chat", m20221019_unread_chat, Nothing), + ("20221021_auto_accept__group_links", m20221021_auto_accept__group_links, Nothing), + ("20221024_contact_used", m20221024_contact_used, Nothing), + ("20221025_chat_settings", m20221025_chat_settings, Nothing), + ("20221029_group_link_id", m20221029_group_link_id, Nothing), + ("20221112_server_password", m20221112_server_password, Nothing), + ("20221115_server_cfg", m20221115_server_cfg, Nothing), + ("20221129_delete_group_feature_items", m20221129_delete_group_feature_items, Nothing), + ("20221130_delete_item_deleted", m20221130_delete_item_deleted, Nothing), + ("20221209_verified_connection", m20221209_verified_connection, Nothing), + ("20221210_idxs", m20221210_idxs, Nothing), + ("20221211_group_description", m20221211_group_description, Nothing), + ("20221212_chat_items_timed", m20221212_chat_items_timed, Nothing), + ("20221214_live_message", m20221214_live_message, Nothing), + ("20221222_chat_ts", m20221222_chat_ts, Nothing), + ("20221223_idx_chat_items_item_status", m20221223_idx_chat_items_item_status, Nothing), + ("20221230_idxs", m20221230_idxs, Nothing), + ("20230107_connections_auth_err_counter", m20230107_connections_auth_err_counter, Nothing), + ("20230111_users_agent_user_id", m20230111_users_agent_user_id, Nothing), + ("20230117_fkey_indexes", m20230117_fkey_indexes, Nothing), + ("20230118_recreate_smp_servers", m20230118_recreate_smp_servers, Nothing), + ("20230129_drop_chat_items_group_idx", m20230129_drop_chat_items_group_idx, Nothing), + ("20230206_item_deleted_by_group_member_id", m20230206_item_deleted_by_group_member_id, Nothing), + ("20230303_group_link_role", m20230303_group_link_role, Nothing), + ("20230317_hidden_profiles", m20230317_hidden_profiles, Just down_m20230317_hidden_profiles) -- ("20230304_file_description", m20230304_file_description) ] -- | The list of migrations in ascending order by date migrations :: [Migration] -migrations = sortBy (compare `on` name) $ map migration schemaMigrations +migrations = sortOn name $ map migration schemaMigrations where - migration (name, query) = Migration {name = name, up = fromQuery query} + migration (name, up, down) = Migration {name, up = fromQuery up, down = fromQuery <$> down} -createChatStore :: FilePath -> String -> Bool -> IO SQLiteStore +createChatStore :: FilePath -> String -> MigrationConfirmation -> IO (Either MigrationError SQLiteStore) createChatStore dbFilePath dbKey = createSQLiteStore dbFilePath dbKey migrations chatStoreFile :: FilePath -> FilePath diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index de787424a2..46912712e6 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -117,7 +117,7 @@ responseToView user_ ChatConfig {logLevel, testView} liveItems ts = \case CRUserProfile u p -> ttyUser u $ viewUserProfile p CRUserProfileNoChange u -> ttyUser u ["user profile did not change"] CRUserPrivacy u -> ttyUserPrefix u $ viewUserPrivacy u - CRVersionInfo info -> viewVersionInfo logLevel info + CRVersionInfo info _ _ -> viewVersionInfo logLevel info CRInvitation u cReq -> ttyUser u $ viewConnReqInvitation cReq CRSentConfirmation u -> ttyUser u ["confirmation sent!"] CRSentInvitation u customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView diff --git a/stack.yaml b/stack.yaml index 62f9790955..ff289f8040 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: d41c2bec2af2aa77e7d671800c08c9760187dff9 + commit: 6a665a083387fe7145d161957f0fcab223a48838 - github: kazu-yamamoto/http2 commit: 78e18f52295a7f89e828539a03fbcb24931461a3 # - ../direct-sqlcipher diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index b914dd2e3b..d5f2d616e6 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -28,6 +28,7 @@ import Simplex.Chat.Terminal.Output (newChatTerminal) import Simplex.Chat.Types (AgentUserId (..), Profile, User (..)) import Simplex.Messaging.Agent.Env.SQLite import Simplex.Messaging.Agent.RetryInterval +import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..)) import Simplex.Messaging.Client (ProtocolClientConfig (..), defaultNetworkConfig) import Simplex.Messaging.Server (runSMPServerBlocking) import Simplex.Messaging.Server.Env.STM @@ -118,13 +119,13 @@ testCfgV1 = testCfg {agentConfig = testAgentCfgV1} createTestChat :: FilePath -> ChatConfig -> ChatOpts -> String -> Profile -> IO TestCC createTestChat tmp cfg opts@ChatOpts {coreOptions = CoreChatOpts {dbKey}} dbPrefix profile = do - db@ChatDatabase {chatStore} <- createChatDatabase (tmp dbPrefix) dbKey False + Right db@ChatDatabase {chatStore} <- createChatDatabase (tmp dbPrefix) dbKey MCError Right user <- withTransaction chatStore $ \db' -> runExceptT $ createUserRecord db' (AgentUserId 1) profile True startTestChat_ db cfg opts user startTestChat :: FilePath -> ChatConfig -> ChatOpts -> String -> IO TestCC startTestChat tmp cfg opts@ChatOpts {coreOptions = CoreChatOpts {dbKey}} dbPrefix = do - db@ChatDatabase {chatStore} <- createChatDatabase (tmp dbPrefix) dbKey False + Right db@ChatDatabase {chatStore} <- createChatDatabase (tmp dbPrefix) dbKey MCError Just user <- find activeUser <$> withTransaction chatStore getUsers startTestChat_ db cfg opts user diff --git a/tests/MobileTests.hs b/tests/MobileTests.hs index 432f2c0243..feaed08a8d 100644 --- a/tests/MobileTests.hs +++ b/tests/MobileTests.hs @@ -7,6 +7,7 @@ import Control.Monad.Except import Simplex.Chat.Mobile import Simplex.Chat.Store import Simplex.Chat.Types (AgentUserId (..), Profile (..)) +import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..)) import System.FilePath (()) import Test.Hspec @@ -85,8 +86,8 @@ parsedMarkdown = "{\"formattedText\":[{\"format\":{\"type\":\"bold\"},\"text\":\ testChatApiNoUser :: FilePath -> IO () testChatApiNoUser tmp = do let dbPrefix = tmp "1" - Right cc <- chatMigrateInit dbPrefix "" - Left (DBMErrorNotADatabase _) <- chatMigrateInit dbPrefix "myKey" + Right cc <- chatMigrateInit dbPrefix "" "yesUp" + Left (DBMErrorNotADatabase _) <- chatMigrateInit dbPrefix "myKey" "yesUp" chatSendCmd cc "/u" `shouldReturn` noActiveUser chatSendCmd cc "/_start" `shouldReturn` noActiveUser chatSendCmd cc "/create user alice Alice" `shouldReturn` activeUser @@ -96,11 +97,11 @@ testChatApi :: FilePath -> IO () testChatApi tmp = do let dbPrefix = tmp "1" f = chatStoreFile dbPrefix - st <- createChatStore f "myKey" True + Right st <- createChatStore f "myKey" MCYesUp Right _ <- withTransaction st $ \db -> runExceptT $ createUserRecord db (AgentUserId 1) aliceProfile {preferences = Nothing} True - Right cc <- chatMigrateInit dbPrefix "myKey" - Left (DBMErrorNotADatabase _) <- chatMigrateInit dbPrefix "" - Left (DBMErrorNotADatabase _) <- chatMigrateInit dbPrefix "anotherKey" + Right cc <- chatMigrateInit dbPrefix "myKey" "yesUp" + Left (DBMErrorNotADatabase _) <- chatMigrateInit dbPrefix "" "yesUp" + Left (DBMErrorNotADatabase _) <- chatMigrateInit dbPrefix "anotherKey" "yesUp" chatSendCmd cc "/u" `shouldReturn` activeUser chatSendCmd cc "/create user alice Alice" `shouldReturn` activeUserExists chatSendCmd cc "/_start" `shouldReturn` chatStarted diff --git a/tests/SchemaDump.hs b/tests/SchemaDump.hs index 4cb2662014..96197f1295 100644 --- a/tests/SchemaDump.hs +++ b/tests/SchemaDump.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} module SchemaDump where @@ -5,26 +6,63 @@ module SchemaDump where import ChatClient (withTmpFiles) import Control.DeepSeq import Control.Monad (void) +import Data.List (dropWhileEnd) +import Data.Maybe (fromJust, isJust) import Simplex.Chat.Store (createChatStore) +import qualified Simplex.Chat.Store as Store +import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), closeSQLiteStore, createSQLiteStore, withConnection) +import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..), MigrationsToRun (..), toDownMigration) +import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations +import Simplex.Messaging.Util (ifM) +import System.Directory (doesFileExist, removeFile) import System.Process (readCreateProcess, shell) import Test.Hspec testDB :: FilePath testDB = "tests/tmp/test_chat.db" -schema :: FilePath -schema = "src/Simplex/Chat/Migrations/chat_schema.sql" +appSchema :: FilePath +appSchema = "src/Simplex/Chat/Migrations/chat_schema.sql" + +testSchema :: FilePath +testSchema = "tests/tmp/test_agent_schema.sql" schemaDumpTest :: Spec -schemaDumpTest = +schemaDumpTest = do it "verify and overwrite schema dump" testVerifySchemaDump + it "verify schema down migrations" testSchemaMigrations testVerifySchemaDump :: IO () testVerifySchemaDump = withTmpFiles $ do - void $ createChatStore testDB "" False - void $ readCreateProcess (shell $ "touch " <> schema) "" - savedSchema <- readFile schema + savedSchema <- ifM (doesFileExist appSchema) (readFile appSchema) (pure "") savedSchema `deepseq` pure () - void $ readCreateProcess (shell $ "sqlite3 " <> testDB <> " '.schema --indent' > " <> schema) "" - currentSchema <- readFile schema - savedSchema `shouldBe` currentSchema + void $ createChatStore testDB "" MCError + getSchema testDB appSchema `shouldReturn` savedSchema + removeFile testDB + +testSchemaMigrations :: IO () +testSchemaMigrations = withTmpFiles $ do + let noDownMigrations = dropWhileEnd (\Migration {down} -> isJust down) Store.migrations + Right st <- createSQLiteStore testDB "" noDownMigrations MCError + mapM_ (testDownMigration st) $ drop (length noDownMigrations) Store.migrations + closeSQLiteStore st + removeFile testDB + removeFile testSchema + where + testDownMigration st m = do + putStrLn $ "down migration " <> name m + let downMigr = fromJust $ toDownMigration m + schema <- getSchema testDB testSchema + withConnection st (`Migrations.run` MTRUp [m]) + schema' <- getSchema testDB testSchema + schema' `shouldNotBe` schema + withConnection st (`Migrations.run` MTRDown [downMigr]) + schema'' <- getSchema testDB testSchema + schema'' `shouldBe` schema + withConnection st (`Migrations.run` MTRUp [m]) + +getSchema :: FilePath -> FilePath -> IO String +getSchema dpPath schemaPath = do + void $ readCreateProcess (shell $ "sqlite3 " <> dpPath <> " '.schema --indent' > " <> schemaPath) "" + sch <- readFile schemaPath + sch `deepseq` pure sch