mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-24 23:55:50 +00:00
core: support down migrations to allow reverting to the previous version (#2072)
* core: support down migrations to allow reverting to the previous version * update schema * update simplexmq * rename errors * remove unused functions * migration UI, test migration * update migration UI * return current migrations in CRVersionInfo * update simplexmq * test down migrations * cleanup ios * show migrations in log
This commit is contained in:
committed by
GitHub
parent
f5c11b8faf
commit
c96ba30018
@@ -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() {
|
||||
|
||||
@@ -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: ()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = "<group>"; };
|
||||
5C65DAF129CBA429003CEE45 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5C65DAF229CBA429003CEE45 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5C65DAF829D0CC20003CEE45 /* DeveloperView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperView.swift; sourceTree = "<group>"; };
|
||||
5C65F341297D3F3600B67AF3 /* VersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionView.swift; sourceTree = "<group>"; };
|
||||
5C6AD81227A834E300348BD7 /* NewChatButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewChatButton.swift; sourceTree = "<group>"; };
|
||||
5C6BA666289BD954009B8ECC /* DismissSheets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissSheets.swift; sourceTree = "<group>"; };
|
||||
@@ -691,6 +693,7 @@
|
||||
64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */,
|
||||
18415845648CA4F5A8BCA272 /* UserProfilesView.swift */,
|
||||
5C65F341297D3F3600B67AF3 /* VersionView.swift */,
|
||||
5C65DAF829D0CC20003CEE45 /* DeveloperView.swift */,
|
||||
);
|
||||
path = UserSettings;
|
||||
sourceTree = "<group>";
|
||||
@@ -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 */,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
+17
-13
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
CREATE TABLE migrations(
|
||||
name TEXT NOT NULL,
|
||||
ts TEXT NOT NULL,
|
||||
down TEXT,
|
||||
PRIMARY KEY(name)
|
||||
);
|
||||
CREATE TABLE contact_profiles(
|
||||
|
||||
+21
-74
@@ -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
|
||||
|
||||
+60
-60
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+3
-2
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+47
-9
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user