mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-28 20:29:53 +00:00
297 lines
10 KiB
Swift
297 lines
10 KiB
Swift
//
|
|
// API.swift
|
|
// SimpleX NSE
|
|
//
|
|
// Created by Evgeny on 26/04/2022.
|
|
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
private var chatController: chat_ctrl?
|
|
|
|
private var migrationResult: (Bool, DBMigrationResult)?
|
|
|
|
public func hasChatCtrl() -> Bool {
|
|
chatController != nil
|
|
}
|
|
|
|
public func getChatCtrl() -> chat_ctrl {
|
|
if let controller = chatController { return controller }
|
|
fatalError("chat controller not initialized")
|
|
}
|
|
|
|
public func chatMigrateInit(_ useKey: String? = nil, confirmMigrations: MigrationConfirmation? = nil, backgroundMode: Bool = false) -> (Bool, DBMigrationResult) {
|
|
if let res = migrationResult { return res }
|
|
let dbPath = getAppDatabasePath().path
|
|
var dbKey = ""
|
|
let useKeychain = storeDBPassphraseGroupDefault.get()
|
|
logger.debug("chatMigrateInit uses keychain: \(useKeychain)")
|
|
if let key = useKey {
|
|
dbKey = key
|
|
} else if useKeychain {
|
|
if !hasDatabase() {
|
|
logger.debug("chatMigrateInit generating a random DB key")
|
|
dbKey = randomDatabasePassword()
|
|
initialRandomDBPassphraseGroupDefault.set(true)
|
|
} else if let key = kcDatabasePassword.get() {
|
|
dbKey = key
|
|
}
|
|
}
|
|
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_key(&cPath, &cKey, 1, &cConfirm, backgroundMode ? 1 : 0, &chatController)!
|
|
let dbRes = dbMigrationResult(fromCString(cjson))
|
|
let encrypted = dbKey != ""
|
|
let keychainErr = dbRes == .ok && useKeychain && encrypted && !kcDatabasePassword.set(dbKey)
|
|
let result = (encrypted, keychainErr ? .errorKeychain : dbRes)
|
|
migrationResult = result
|
|
return result
|
|
}
|
|
|
|
public func chatInitTemporaryDatabase(url: URL, key: String? = nil, confirmation: MigrationConfirmation = .error) -> (DBMigrationResult, chat_ctrl?) {
|
|
let dbPath = url.path
|
|
let dbKey = key ?? randomDatabasePassword()
|
|
logger.debug("chatInitTemporaryDatabase path: \(dbPath)")
|
|
var temporaryController: chat_ctrl? = nil
|
|
var cPath = dbPath.cString(using: .utf8)!
|
|
var cKey = dbKey.cString(using: .utf8)!
|
|
var cConfirm = confirmation.rawValue.cString(using: .utf8)!
|
|
let cjson = chat_migrate_init_key(&cPath, &cKey, 1, &cConfirm, 0, &temporaryController)!
|
|
return (dbMigrationResult(fromCString(cjson)), temporaryController)
|
|
}
|
|
|
|
public func chatInitControllerRemovingDatabases() {
|
|
let dbPath = getAppDatabasePath().path
|
|
let fm = FileManager.default
|
|
// Remove previous databases, otherwise, can be .errorNotADatabase with nil controller
|
|
try? fm.removeItem(atPath: dbPath + CHAT_DB)
|
|
try? fm.removeItem(atPath: dbPath + AGENT_DB)
|
|
|
|
let dbKey = randomDatabasePassword()
|
|
logger.debug("chatInitControllerRemovingDatabases path: \(dbPath)")
|
|
var cPath = dbPath.cString(using: .utf8)!
|
|
var cKey = dbKey.cString(using: .utf8)!
|
|
var cConfirm = MigrationConfirmation.error.rawValue.cString(using: .utf8)!
|
|
chat_migrate_init_key(&cPath, &cKey, 1, &cConfirm, 0, &chatController)
|
|
|
|
// We need only controller, not databases
|
|
try? fm.removeItem(atPath: dbPath + CHAT_DB)
|
|
try? fm.removeItem(atPath: dbPath + AGENT_DB)
|
|
}
|
|
|
|
|
|
public func chatCloseStore() {
|
|
// Prevent crash when exiting the app with already closed store (for example, after changing a database passpharase)
|
|
guard hasChatCtrl() else {
|
|
logger.error("chatCloseStore: already closed, chatCtrl is nil")
|
|
return
|
|
}
|
|
let err = fromCString(chat_close_store(getChatCtrl()))
|
|
if err != "" {
|
|
logger.error("chatCloseStore error: \(err)")
|
|
}
|
|
}
|
|
|
|
public func chatReopenStore() {
|
|
let err = fromCString(chat_reopen_store(getChatCtrl()))
|
|
if err != "" {
|
|
logger.error("chatReopenStore error: \(err)")
|
|
}
|
|
}
|
|
|
|
public func resetChatCtrl() {
|
|
chatController = nil
|
|
migrationResult = nil
|
|
}
|
|
|
|
public func sendSimpleXCmd<CR: ChatRespProtocol>(_ cmd: ChatCmdProtocol, _ ctrl: chat_ctrl? = nil) -> CR {
|
|
var c = cmd.cmdString.cString(using: .utf8)!
|
|
let cjson = chat_send_cmd(ctrl ?? getChatCtrl(), &c)!
|
|
return CR.chatResponse(fromCString(cjson))
|
|
}
|
|
|
|
// in microseconds
|
|
public let MESSAGE_TIMEOUT: Int32 = 15_000_000
|
|
|
|
public func recvSimpleXMsg<CR: ChatRespProtocol>(_ ctrl: chat_ctrl? = nil, messageTimeout: Int32 = MESSAGE_TIMEOUT) -> CR? {
|
|
if let cjson = chat_recv_msg_wait(ctrl ?? getChatCtrl(), messageTimeout) {
|
|
let s = fromCString(cjson)
|
|
return s == "" ? nil : CR.chatResponse(s)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
public func parseSimpleXMarkdown(_ s: String) -> [FormattedText]? {
|
|
var c = s.cString(using: .utf8)!
|
|
if let cjson = chat_parse_markdown(&c) {
|
|
if let d = fromCString(cjson).data(using: .utf8) {
|
|
do {
|
|
let r = try jsonDecoder.decode(ParsedMarkdown.self, from: d)
|
|
return r.formattedText
|
|
} catch {
|
|
logger.error("parseSimpleXMarkdown jsonDecoder.decode error: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
public func chatJsonLength(_ s: String) -> Int {
|
|
var c = s.cString(using: .utf8)!
|
|
return Int(chat_json_length(&c))
|
|
}
|
|
|
|
struct ParsedMarkdown: Decodable {
|
|
var formattedText: [FormattedText]?
|
|
}
|
|
|
|
public func parseServerAddress(_ s: String) -> ServerAddress? {
|
|
var c = s.cString(using: .utf8)!
|
|
if let cjson = chat_parse_server(&c) {
|
|
if let d = fromCString(cjson).data(using: .utf8) {
|
|
do {
|
|
let r = try jsonDecoder.decode(ParsedServerAddress.self, from: d)
|
|
return r.serverAddress
|
|
} catch {
|
|
logger.error("parseServerAddress jsonDecoder.decode error: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
struct ParsedServerAddress: Decodable {
|
|
var serverAddress: ServerAddress?
|
|
var parseError: String
|
|
}
|
|
|
|
public func fromCString(_ c: UnsafeMutablePointer<CChar>) -> String {
|
|
let s = String.init(cString: c)
|
|
free(c)
|
|
return s
|
|
}
|
|
|
|
public func decodeUser_(_ jDict: NSDictionary) -> UserRef? {
|
|
if let user_ = jDict["user_"] {
|
|
try? decodeObject(user_ as Any)
|
|
} else {
|
|
nil
|
|
}
|
|
}
|
|
|
|
public func errorJson(_ jDict: NSDictionary) -> String? {
|
|
if let chatError = jDict["chatError"] {
|
|
serializeJSON(chatError)
|
|
} else {
|
|
serializeJSON(jDict)
|
|
}
|
|
}
|
|
|
|
public func parseChatData(_ jChat: Any, _ jNavInfo: Any? = nil) throws -> (ChatData, NavigationInfo) {
|
|
let jChatDict = jChat as! NSDictionary
|
|
let chatInfo: ChatInfo = try decodeObject(jChatDict["chatInfo"]!)
|
|
let chatStats: ChatStats = try decodeObject(jChatDict["chatStats"]!)
|
|
let navInfo: NavigationInfo = jNavInfo == nil ? NavigationInfo() : try decodeObject((jNavInfo as! NSDictionary)["navInfo"]!)
|
|
let jChatItems = jChatDict["chatItems"] as! NSArray
|
|
let chatItems = jChatItems.map { jCI in
|
|
if let ci: ChatItem = try? decodeObject(jCI) {
|
|
return ci
|
|
}
|
|
return ChatItem.invalidJSON(
|
|
chatDir: decodeProperty(jCI, "chatDir"),
|
|
meta: decodeProperty(jCI, "meta"),
|
|
json: serializeJSON(jCI, options: .prettyPrinted) ?? ""
|
|
)
|
|
}
|
|
return (ChatData(chatInfo: chatInfo, chatItems: chatItems, chatStats: chatStats), navInfo)
|
|
}
|
|
|
|
public func decodeObject<T: Decodable>(_ obj: Any) throws -> T {
|
|
try jsonDecoder.decode(T.self, from: JSONSerialization.data(withJSONObject: obj))
|
|
}
|
|
|
|
func decodeProperty<T: Decodable>(_ obj: Any, _ prop: NSString) -> T? {
|
|
if let jProp = (obj as? NSDictionary)?[prop] {
|
|
return try? decodeObject(jProp)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
public func serializeJSON(_ obj: Any, options: JSONSerialization.WritingOptions = []) -> String? {
|
|
if let d = try? JSONSerialization.data(withJSONObject: obj, options: options) {
|
|
return String(decoding: d, as: UTF8.self)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
public func responseError(_ err: Error) -> String {
|
|
if let r = err as? ChatRespProtocol {
|
|
if let e = r.chatError {
|
|
chatErrorString(e)
|
|
} else {
|
|
"\(String(describing: r.responseType)), details: \(String(describing: r.details))"
|
|
}
|
|
} else {
|
|
String(describing: err)
|
|
}
|
|
}
|
|
|
|
public func chatErrorString(_ err: ChatError) -> String {
|
|
if case let .invalidJSON(json) = err { return json }
|
|
return String(describing: err)
|
|
}
|
|
|
|
public enum DBMigrationResult: Decodable, Equatable {
|
|
case ok
|
|
case invalidConfirmation
|
|
case errorNotADatabase(dbFile: 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:
|
|
// let p = UnsafeMutableRawPointer.init(mutating: UnsafeRawPointer(cjson))
|
|
// let d = Data.init(bytesNoCopy: p, count: strlen(cjson), deallocator: .free)
|
|
do {
|
|
return try jsonDecoder.decode(DBMigrationResult.self, from: d)
|
|
} catch let error {
|
|
logger.error("chatResponse jsonDecoder.decode error: \(error.localizedDescription)")
|
|
return .unknown(json: s)
|
|
}
|
|
}
|