2022-05-03 08:20:19 +01:00
|
|
|
//
|
|
|
|
// 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?
|
|
|
|
|
2022-09-23 12:51:40 +01:00
|
|
|
private var migrationResult: (Bool, DBMigrationResult)?
|
|
|
|
|
2024-01-08 10:56:01 +00:00
|
|
|
public func hasChatCtrl() -> Bool {
|
|
|
|
chatController != nil
|
|
|
|
}
|
|
|
|
|
|
|
|
public func getChatCtrl() -> chat_ctrl {
|
2022-05-03 08:20:19 +01:00
|
|
|
if let controller = chatController { return controller }
|
2022-09-24 09:28:22 +01:00
|
|
|
fatalError("chat controller not initialized")
|
2022-05-03 08:20:19 +01:00
|
|
|
}
|
|
|
|
|
2023-12-23 13:06:59 +00:00
|
|
|
public func chatMigrateInit(_ useKey: String? = nil, confirmMigrations: MigrationConfirmation? = nil, backgroundMode: Bool = false) -> (Bool, DBMigrationResult) {
|
2022-09-23 12:51:40 +01:00
|
|
|
if let res = migrationResult { return res }
|
2022-09-07 12:49:41 +01:00
|
|
|
let dbPath = getAppDatabasePath().path
|
|
|
|
var dbKey = ""
|
|
|
|
let useKeychain = storeDBPassphraseGroupDefault.get()
|
2022-09-25 20:53:32 +01:00
|
|
|
logger.debug("chatMigrateInit uses keychain: \(useKeychain)")
|
2022-09-07 12:49:41 +01:00
|
|
|
if let key = useKey {
|
|
|
|
dbKey = key
|
|
|
|
} else if useKeychain {
|
|
|
|
if !hasDatabase() {
|
2022-09-25 20:53:32 +01:00
|
|
|
logger.debug("chatMigrateInit generating a random DB key")
|
2022-09-07 12:49:41 +01:00
|
|
|
dbKey = randomDatabasePassword()
|
|
|
|
initialRandomDBPassphraseGroupDefault.set(true)
|
2023-04-12 12:22:55 +02:00
|
|
|
} else if let key = kcDatabasePassword.get() {
|
2022-09-07 12:49:41 +01:00
|
|
|
dbKey = key
|
|
|
|
}
|
|
|
|
}
|
2023-03-27 18:34:48 +01:00
|
|
|
let confirm = confirmMigrations ?? defaultMigrationConfirmation()
|
|
|
|
logger.debug("chatMigrateInit DB path: \(dbPath), confirm: \(confirm.rawValue)")
|
2022-09-25 13:17:04 +01:00
|
|
|
// logger.debug("chatMigrateInit DB key: \(dbKey)")
|
2022-09-07 12:49:41 +01:00
|
|
|
var cPath = dbPath.cString(using: .utf8)!
|
|
|
|
var cKey = dbKey.cString(using: .utf8)!
|
2023-03-27 18:34:48 +01:00
|
|
|
var cConfirm = confirm.rawValue.cString(using: .utf8)!
|
2022-09-24 09:28:22 +01:00
|
|
|
// the last parameter of chat_migrate_init is used to return the pointer to chat controller
|
2023-12-23 13:06:59 +00:00
|
|
|
let cjson = chat_migrate_init_key(&cPath, &cKey, 1, &cConfirm, backgroundMode ? 1 : 0, &chatController)!
|
2022-09-23 12:51:40 +01:00
|
|
|
let dbRes = dbMigrationResult(fromCString(cjson))
|
2022-09-07 12:49:41 +01:00
|
|
|
let encrypted = dbKey != ""
|
2023-04-12 12:22:55 +02:00
|
|
|
let keychainErr = dbRes == .ok && useKeychain && encrypted && !kcDatabasePassword.set(dbKey)
|
2022-09-23 12:51:40 +01:00
|
|
|
let result = (encrypted, keychainErr ? .errorKeychain : dbRes)
|
|
|
|
migrationResult = result
|
|
|
|
return result
|
2022-09-07 12:49:41 +01:00
|
|
|
}
|
|
|
|
|
2024-03-11 21:17:28 +07:00
|
|
|
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
|
2024-03-12 23:25:06 +07:00
|
|
|
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)
|
|
|
|
|
2024-03-11 21:17:28 +07:00
|
|
|
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)
|
2024-03-12 23:25:06 +07:00
|
|
|
|
2024-03-11 21:17:28 +07:00
|
|
|
// We need only controller, not databases
|
|
|
|
try? fm.removeItem(atPath: dbPath + CHAT_DB)
|
|
|
|
try? fm.removeItem(atPath: dbPath + AGENT_DB)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-10-09 18:03:03 +01:00
|
|
|
public func chatCloseStore() {
|
|
|
|
let err = fromCString(chat_close_store(getChatCtrl()))
|
|
|
|
if err != "" {
|
|
|
|
logger.error("chatCloseStore error: \(err)")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-09 21:59:40 +00:00
|
|
|
public func chatReopenStore() {
|
|
|
|
let err = fromCString(chat_reopen_store(getChatCtrl()))
|
|
|
|
if err != "" {
|
|
|
|
logger.error("chatReopenStore error: \(err)")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-24 13:52:20 +01:00
|
|
|
public func resetChatCtrl() {
|
|
|
|
chatController = nil
|
2022-09-23 12:51:40 +01:00
|
|
|
migrationResult = nil
|
2022-06-24 13:52:20 +01:00
|
|
|
}
|
|
|
|
|
2024-03-11 21:17:28 +07:00
|
|
|
public func sendSimpleXCmd(_ cmd: ChatCommand, _ ctrl: chat_ctrl? = nil) -> ChatResponse {
|
2022-05-03 08:20:19 +01:00
|
|
|
var c = cmd.cmdString.cString(using: .utf8)!
|
2024-03-11 21:17:28 +07:00
|
|
|
let cjson = chat_send_cmd(ctrl ?? getChatCtrl(), &c)!
|
2022-06-24 13:52:20 +01:00
|
|
|
return chatResponse(fromCString(cjson))
|
|
|
|
}
|
|
|
|
|
|
|
|
// in microseconds
|
|
|
|
let MESSAGE_TIMEOUT: Int32 = 15_000_000
|
|
|
|
|
2024-03-11 21:17:28 +07:00
|
|
|
public func recvSimpleXMsg(_ ctrl: chat_ctrl? = nil) -> ChatResponse? {
|
|
|
|
if let cjson = chat_recv_msg_wait(ctrl ?? getChatCtrl(), MESSAGE_TIMEOUT) {
|
2022-06-24 13:52:20 +01:00
|
|
|
let s = fromCString(cjson)
|
|
|
|
return s == "" ? nil : 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
|
|
|
|
}
|
|
|
|
|
2024-01-26 13:37:49 +04:00
|
|
|
public func chatJsonLength(_ s: String) -> Int {
|
|
|
|
var c = s.cString(using: .utf8)!
|
|
|
|
return Int(chat_json_length(&c))
|
|
|
|
}
|
|
|
|
|
2022-06-24 13:52:20 +01:00
|
|
|
struct ParsedMarkdown: Decodable {
|
|
|
|
var formattedText: [FormattedText]?
|
|
|
|
}
|
|
|
|
|
2022-11-21 08:37:13 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-03-02 16:17:01 +03:00
|
|
|
public func fromCString(_ c: UnsafeMutablePointer<CChar>) -> String {
|
2022-06-24 13:52:20 +01:00
|
|
|
let s = String.init(cString: c)
|
|
|
|
free(c)
|
|
|
|
return s
|
2022-05-03 08:20:19 +01:00
|
|
|
}
|
|
|
|
|
2022-06-24 13:52:20 +01:00
|
|
|
public func chatResponse(_ s: String) -> ChatResponse {
|
2022-05-03 08:20:19 +01:00
|
|
|
let d = s.data(using: .utf8)!
|
2022-12-29 18:15:19 +04:00
|
|
|
// 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)
|
2022-05-03 08:20:19 +01:00
|
|
|
do {
|
|
|
|
let r = try jsonDecoder.decode(APIResponse.self, from: d)
|
|
|
|
return r.resp
|
|
|
|
} catch {
|
|
|
|
logger.error("chatResponse jsonDecoder.decode error: \(error.localizedDescription)")
|
|
|
|
}
|
2022-12-29 18:15:19 +04:00
|
|
|
|
2022-05-03 08:20:19 +01:00
|
|
|
var type: String?
|
|
|
|
var json: String?
|
|
|
|
if let j = try? JSONSerialization.jsonObject(with: d) as? NSDictionary {
|
2023-10-09 16:56:42 +01:00
|
|
|
if let jResp = j["resp"] as? NSDictionary, jResp.count == 1 || jResp.count == 2 {
|
2022-12-29 18:15:19 +04:00
|
|
|
type = jResp.allKeys[0] as? String
|
2023-10-09 16:56:42 +01:00
|
|
|
if jResp.count == 2 && type == "_owsf" {
|
|
|
|
type = jResp.allKeys[1] as? String
|
|
|
|
}
|
2022-12-29 18:15:19 +04:00
|
|
|
if type == "apiChats" {
|
|
|
|
if let jApiChats = jResp["apiChats"] as? NSDictionary,
|
2023-08-22 12:20:21 +01:00
|
|
|
let user: UserRef = try? decodeObject(jApiChats["user"] as Any),
|
2022-12-29 18:15:19 +04:00
|
|
|
let jChats = jApiChats["chats"] as? NSArray {
|
|
|
|
let chats = jChats.map { jChat in
|
|
|
|
if let chatData = try? parseChatData(jChat) {
|
|
|
|
return chatData
|
|
|
|
}
|
|
|
|
return ChatData.invalidJSON(prettyJSON(jChat) ?? "")
|
|
|
|
}
|
2023-01-19 16:22:56 +00:00
|
|
|
return .apiChats(user: user, chats: chats)
|
2022-12-29 18:15:19 +04:00
|
|
|
}
|
|
|
|
} else if type == "apiChat" {
|
|
|
|
if let jApiChat = jResp["apiChat"] as? NSDictionary,
|
2023-08-22 12:20:21 +01:00
|
|
|
let user: UserRef = try? decodeObject(jApiChat["user"] as Any),
|
2022-12-29 18:15:19 +04:00
|
|
|
let jChat = jApiChat["chat"] as? NSDictionary,
|
|
|
|
let chat = try? parseChatData(jChat) {
|
2023-01-19 16:22:56 +00:00
|
|
|
return .apiChat(user: user, chat: chat)
|
2022-12-29 18:15:19 +04:00
|
|
|
}
|
2023-03-22 15:58:01 +00:00
|
|
|
} else if type == "chatCmdError" {
|
|
|
|
if let jError = jResp["chatCmdError"] as? NSDictionary {
|
2024-03-18 22:38:49 +04:00
|
|
|
return .chatCmdError(user_: decodeUser_(jError), chatError: .invalidJSON(json: prettyJSON(jError) ?? ""))
|
2023-03-22 15:58:01 +00:00
|
|
|
}
|
2023-08-22 12:20:21 +01:00
|
|
|
} else if type == "chatError" {
|
|
|
|
if let jError = jResp["chatError"] as? NSDictionary {
|
2024-03-18 22:38:49 +04:00
|
|
|
return .chatError(user_: decodeUser_(jError), chatError: .invalidJSON(json: prettyJSON(jError) ?? ""))
|
2023-08-22 12:20:21 +01:00
|
|
|
}
|
2022-12-29 18:15:19 +04:00
|
|
|
}
|
2022-05-03 08:20:19 +01:00
|
|
|
}
|
|
|
|
json = prettyJSON(j)
|
|
|
|
}
|
|
|
|
return ChatResponse.response(type: type ?? "invalid", json: json ?? s)
|
|
|
|
}
|
|
|
|
|
2024-03-18 22:38:49 +04:00
|
|
|
private func decodeUser_(_ jDict: NSDictionary) -> UserRef? {
|
|
|
|
if let user_ = jDict["user_"] {
|
|
|
|
try? decodeObject(user_ as Any)
|
|
|
|
} else {
|
|
|
|
nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-29 18:15:19 +04:00
|
|
|
func parseChatData(_ jChat: Any) throws -> ChatData {
|
|
|
|
let jChatDict = jChat as! NSDictionary
|
|
|
|
let chatInfo: ChatInfo = try decodeObject(jChatDict["chatInfo"]!)
|
|
|
|
let chatStats: ChatStats = try decodeObject(jChatDict["chatStats"]!)
|
|
|
|
let jChatItems = jChatDict["chatItems"] as! NSArray
|
|
|
|
let chatItems = jChatItems.map { jCI in
|
|
|
|
if let ci: ChatItem = try? decodeObject(jCI) {
|
|
|
|
return ci
|
|
|
|
}
|
2023-04-16 19:30:25 +02:00
|
|
|
return ChatItem.invalidJSON(
|
|
|
|
chatDir: decodeProperty(jCI, "chatDir"),
|
|
|
|
meta: decodeProperty(jCI, "meta"),
|
|
|
|
json: prettyJSON(jCI) ?? ""
|
|
|
|
)
|
2022-12-29 18:15:19 +04:00
|
|
|
}
|
|
|
|
return ChatData(chatInfo: chatInfo, chatItems: chatItems, chatStats: chatStats)
|
|
|
|
}
|
|
|
|
|
|
|
|
func decodeObject<T: Decodable>(_ obj: Any) throws -> T {
|
|
|
|
try jsonDecoder.decode(T.self, from: JSONSerialization.data(withJSONObject: obj))
|
|
|
|
}
|
|
|
|
|
2023-04-16 19:30:25 +02:00
|
|
|
func decodeProperty<T: Decodable>(_ obj: Any, _ prop: NSString) -> T? {
|
|
|
|
if let jProp = (obj as? NSDictionary)?[prop] {
|
|
|
|
return try? decodeObject(jProp)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-12-29 18:15:19 +04:00
|
|
|
func prettyJSON(_ obj: Any) -> String? {
|
2022-05-03 08:20:19 +01:00
|
|
|
if let d = try? JSONSerialization.data(withJSONObject: obj, options: .prettyPrinted) {
|
|
|
|
return String(decoding: d, as: UTF8.self)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-05-31 07:55:13 +01:00
|
|
|
public func responseError(_ err: Error) -> String {
|
2022-05-03 08:20:19 +01:00
|
|
|
if let r = err as? ChatResponse {
|
2023-03-22 15:58:01 +00:00
|
|
|
switch r {
|
|
|
|
case let .chatCmdError(_, chatError): return chatErrorString(chatError)
|
|
|
|
case let .chatError(_, chatError): return chatErrorString(chatError)
|
2023-08-02 11:10:29 +04:00
|
|
|
default: return "\(String(describing: r.responseType)), details: \(String(describing: r.details))"
|
2023-03-22 15:58:01 +00:00
|
|
|
}
|
2022-05-03 08:20:19 +01:00
|
|
|
} else {
|
2023-03-22 15:58:01 +00:00
|
|
|
return String(describing: err)
|
2022-05-03 08:20:19 +01:00
|
|
|
}
|
|
|
|
}
|
2022-09-07 12:49:41 +01:00
|
|
|
|
2023-03-22 15:58:01 +00:00
|
|
|
func chatErrorString(_ err: ChatError) -> String {
|
|
|
|
if case let .invalidJSON(json) = err { return json }
|
|
|
|
return String(describing: err)
|
|
|
|
}
|
|
|
|
|
2022-09-07 12:49:41 +01:00
|
|
|
public enum DBMigrationResult: Decodable, Equatable {
|
|
|
|
case ok
|
2023-03-27 18:34:48 +01:00
|
|
|
case invalidConfirmation
|
2022-09-07 12:49:41 +01:00
|
|
|
case errorNotADatabase(dbFile: String)
|
2023-03-27 18:34:48 +01:00
|
|
|
case errorMigration(dbFile: String, migrationError: MigrationError)
|
|
|
|
case errorSQL(dbFile: String, migrationSQLError: String)
|
2022-09-07 12:49:41 +01:00
|
|
|
case errorKeychain
|
|
|
|
case unknown(json: String)
|
|
|
|
}
|
|
|
|
|
2023-03-27 18:34:48 +01:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2022-09-07 12:49:41 +01:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|