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:
Evgeny Poberezkin
2023-03-27 18:34:48 +01:00
committed by GitHub
parent f5c11b8faf
commit c96ba30018
26 changed files with 365 additions and 222 deletions
+3 -3
View File
@@ -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 */,
+34 -4
View File
@@ -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:
+2 -2
View File
@@ -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))
+4
View File
@@ -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 {
+5 -1
View File
@@ -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")
}
+1 -1
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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
+3 -3
View File
@@ -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}
+7 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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