2024-07-28 17:54:58 +01:00
|
|
|
//
|
|
|
|
// ShareAPI.swift
|
|
|
|
// SimpleX SE
|
|
|
|
//
|
|
|
|
// Created by User on 15/07/2024.
|
|
|
|
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import OSLog
|
|
|
|
import Foundation
|
|
|
|
import SimpleXChat
|
|
|
|
|
|
|
|
let logger = Logger()
|
|
|
|
|
|
|
|
func apiGetActiveUser() throws -> User? {
|
2025-05-02 12:27:08 +01:00
|
|
|
let r: SEChatResponse = sendSimpleXCmd(SEChatCommand.showActiveUser)
|
2024-07-28 17:54:58 +01:00
|
|
|
switch r {
|
|
|
|
case let .activeUser(user): return user
|
|
|
|
case .chatCmdError(_, .error(.noActiveUser)): return nil
|
|
|
|
default: throw r
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func apiStartChat() throws -> Bool {
|
2025-05-02 12:27:08 +01:00
|
|
|
let r: SEChatResponse = sendSimpleXCmd(SEChatCommand.startChat(mainApp: false, enableSndFiles: true))
|
2024-07-28 17:54:58 +01:00
|
|
|
switch r {
|
|
|
|
case .chatStarted: return true
|
|
|
|
case .chatRunning: return false
|
|
|
|
default: throw r
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func apiSetNetworkConfig(_ cfg: NetCfg) throws {
|
2025-05-02 12:27:08 +01:00
|
|
|
let r: SEChatResponse = sendSimpleXCmd(SEChatCommand.apiSetNetworkConfig(networkConfig: cfg))
|
2024-07-28 17:54:58 +01:00
|
|
|
if case .cmdOk = r { return }
|
|
|
|
throw r
|
|
|
|
}
|
|
|
|
|
|
|
|
func apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String) throws {
|
2025-05-02 12:27:08 +01:00
|
|
|
let r: SEChatResponse = sendSimpleXCmd(SEChatCommand.apiSetAppFilePaths(filesFolder: filesFolder, tempFolder: tempFolder, assetsFolder: assetsFolder))
|
2024-07-28 17:54:58 +01:00
|
|
|
if case .cmdOk = r { return }
|
|
|
|
throw r
|
|
|
|
}
|
|
|
|
|
|
|
|
func apiSetEncryptLocalFiles(_ enable: Bool) throws {
|
2025-05-02 12:27:08 +01:00
|
|
|
let r: SEChatResponse = sendSimpleXCmd(SEChatCommand.apiSetEncryptLocalFiles(enable: enable))
|
2024-07-28 17:54:58 +01:00
|
|
|
if case .cmdOk = r { return }
|
|
|
|
throw r
|
|
|
|
}
|
|
|
|
|
|
|
|
func apiGetChats(userId: User.ID) throws -> Array<ChatData> {
|
2025-05-02 12:27:08 +01:00
|
|
|
let r: SEChatResponse = sendSimpleXCmd(SEChatCommand.apiGetChats(userId: userId))
|
2024-07-28 17:54:58 +01:00
|
|
|
if case let .apiChats(user: _, chats: chats) = r { return chats }
|
|
|
|
throw r
|
|
|
|
}
|
|
|
|
|
2024-08-22 21:38:22 +04:00
|
|
|
func apiSendMessages(
|
2024-07-28 17:54:58 +01:00
|
|
|
chatInfo: ChatInfo,
|
2024-08-22 21:38:22 +04:00
|
|
|
composedMessages: [ComposedMessage]
|
|
|
|
) throws -> [AChatItem] {
|
2025-05-02 12:27:08 +01:00
|
|
|
let r: SEChatResponse = sendSimpleXCmd(
|
2024-07-28 17:54:58 +01:00
|
|
|
chatInfo.chatType == .local
|
2025-05-02 12:27:08 +01:00
|
|
|
? SEChatCommand.apiCreateChatItems(
|
2024-07-28 17:54:58 +01:00
|
|
|
noteFolderId: chatInfo.apiId,
|
2024-08-22 21:38:22 +04:00
|
|
|
composedMessages: composedMessages
|
2024-07-28 17:54:58 +01:00
|
|
|
)
|
2025-05-02 12:27:08 +01:00
|
|
|
: SEChatCommand.apiSendMessages(
|
2024-07-28 17:54:58 +01:00
|
|
|
type: chatInfo.chatType,
|
|
|
|
id: chatInfo.apiId,
|
|
|
|
live: false,
|
2024-08-22 21:38:22 +04:00
|
|
|
ttl: nil,
|
|
|
|
composedMessages: composedMessages
|
2024-07-28 17:54:58 +01:00
|
|
|
)
|
|
|
|
)
|
2024-08-22 21:38:22 +04:00
|
|
|
if case let .newChatItems(_, chatItems) = r {
|
|
|
|
return chatItems
|
2024-07-28 17:54:58 +01:00
|
|
|
} else {
|
2024-08-22 21:38:22 +04:00
|
|
|
for composedMessage in composedMessages {
|
|
|
|
if let filePath = composedMessage.fileSource?.filePath { removeFile(filePath) }
|
|
|
|
}
|
2024-07-28 17:54:58 +01:00
|
|
|
throw r
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func apiActivateChat() throws {
|
|
|
|
chatReopenStore()
|
2025-05-02 12:27:08 +01:00
|
|
|
let r: SEChatResponse = sendSimpleXCmd(SEChatCommand.apiActivateChat(restoreChat: false))
|
2024-07-28 17:54:58 +01:00
|
|
|
if case .cmdOk = r { return }
|
|
|
|
throw r
|
|
|
|
}
|
|
|
|
|
|
|
|
func apiSuspendChat(expired: Bool) {
|
2025-05-02 12:27:08 +01:00
|
|
|
let r: SEChatResponse = sendSimpleXCmd(SEChatCommand.apiSuspendChat(timeoutMicroseconds: expired ? 0 : 3_000000))
|
2024-07-28 17:54:58 +01:00
|
|
|
// Block until `chatSuspended` received or 3 seconds has passed
|
|
|
|
var suspended = false
|
|
|
|
if case .cmdOk = r, !expired {
|
|
|
|
let startTime = CFAbsoluteTimeGetCurrent()
|
|
|
|
while CFAbsoluteTimeGetCurrent() - startTime < 3 {
|
2025-05-04 22:14:36 +01:00
|
|
|
let msg: SEChatEvent? = recvSimpleXMsg(messageTimeout: 3_500000)
|
2025-05-02 12:27:08 +01:00
|
|
|
switch msg {
|
2024-07-28 17:54:58 +01:00
|
|
|
case .chatSuspended:
|
|
|
|
suspended = false
|
|
|
|
break
|
|
|
|
default: continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !suspended {
|
2025-05-02 12:27:08 +01:00
|
|
|
let _r1: SEChatResponse = sendSimpleXCmd(SEChatCommand.apiSuspendChat(timeoutMicroseconds: 0))
|
2024-07-28 17:54:58 +01:00
|
|
|
}
|
|
|
|
logger.debug("close store")
|
|
|
|
chatCloseStore()
|
|
|
|
SEChatState.shared.set(.inactive)
|
|
|
|
}
|
2025-05-02 12:27:08 +01:00
|
|
|
|
|
|
|
enum SEChatCommand: ChatCmdProtocol {
|
|
|
|
case showActiveUser
|
|
|
|
case startChat(mainApp: Bool, enableSndFiles: Bool)
|
|
|
|
case apiActivateChat(restoreChat: Bool)
|
|
|
|
case apiSuspendChat(timeoutMicroseconds: Int)
|
|
|
|
case apiSetNetworkConfig(networkConfig: NetCfg)
|
|
|
|
case apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String)
|
|
|
|
case apiSetEncryptLocalFiles(enable: Bool)
|
|
|
|
case apiGetChats(userId: Int64)
|
|
|
|
case apiCreateChatItems(noteFolderId: Int64, composedMessages: [ComposedMessage])
|
|
|
|
case apiSendMessages(type: ChatType, id: Int64, live: Bool, ttl: Int?, composedMessages: [ComposedMessage])
|
|
|
|
|
|
|
|
var cmdString: String {
|
|
|
|
switch self {
|
|
|
|
case .showActiveUser: return "/u"
|
|
|
|
case let .startChat(mainApp, enableSndFiles): return "/_start main=\(onOff(mainApp)) snd_files=\(onOff(enableSndFiles))"
|
|
|
|
case let .apiActivateChat(restore): return "/_app activate restore=\(onOff(restore))"
|
|
|
|
case let .apiSuspendChat(timeoutMicroseconds): return "/_app suspend \(timeoutMicroseconds)"
|
|
|
|
case let .apiSetNetworkConfig(networkConfig): return "/_network \(encodeJSON(networkConfig))"
|
|
|
|
case let .apiSetAppFilePaths(filesFolder, tempFolder, assetsFolder):
|
|
|
|
return "/set file paths \(encodeJSON(AppFilePaths(appFilesFolder: filesFolder, appTempFolder: tempFolder, appAssetsFolder: assetsFolder)))"
|
|
|
|
case let .apiSetEncryptLocalFiles(enable): return "/_files_encrypt \(onOff(enable))"
|
|
|
|
case let .apiGetChats(userId): return "/_get chats \(userId) pcc=on"
|
|
|
|
case let .apiCreateChatItems(noteFolderId, composedMessages):
|
|
|
|
let msgs = encodeJSON(composedMessages)
|
|
|
|
return "/_create *\(noteFolderId) json \(msgs)"
|
|
|
|
case let .apiSendMessages(type, id, live, ttl, composedMessages):
|
|
|
|
let msgs = encodeJSON(composedMessages)
|
|
|
|
let ttlStr = ttl != nil ? "\(ttl!)" : "default"
|
|
|
|
return "/_send \(ref(type, id)) live=\(onOff(live)) ttl=\(ttlStr) json \(msgs)"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func ref(_ type: ChatType, _ id: Int64) -> String {
|
|
|
|
"\(type.rawValue)\(id)"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
enum SEChatResponse: Decodable, Error, ChatRespProtocol {
|
|
|
|
case response(type: String, json: String)
|
|
|
|
case activeUser(user: User)
|
|
|
|
case chatStarted
|
|
|
|
case chatRunning
|
|
|
|
case apiChats(user: UserRef, chats: [ChatData])
|
|
|
|
case newChatItems(user: UserRef, chatItems: [AChatItem])
|
|
|
|
case cmdOk(user_: UserRef?)
|
|
|
|
case chatCmdError(user_: UserRef?, chatError: ChatError)
|
|
|
|
|
|
|
|
var responseType: String {
|
|
|
|
switch self {
|
|
|
|
case let .response(type, _): "* \(type)"
|
|
|
|
case .activeUser: "activeUser"
|
|
|
|
case .chatStarted: "chatStarted"
|
|
|
|
case .chatRunning: "chatRunning"
|
|
|
|
case .apiChats: "apiChats"
|
|
|
|
case .newChatItems: "newChatItems"
|
|
|
|
case .cmdOk: "cmdOk"
|
|
|
|
case .chatCmdError: "chatCmdError"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var details: String {
|
|
|
|
switch self {
|
|
|
|
case let .response(_, json): return json
|
|
|
|
case let .activeUser(user): return String(describing: user)
|
|
|
|
case .chatStarted: return noDetails
|
|
|
|
case .chatRunning: return noDetails
|
|
|
|
case let .apiChats(u, chats): return withUser(u, String(describing: chats))
|
|
|
|
case let .newChatItems(u, chatItems):
|
|
|
|
let itemsString = chatItems.map { chatItem in String(describing: chatItem) }.joined(separator: "\n")
|
|
|
|
return withUser(u, itemsString)
|
|
|
|
case .cmdOk: return noDetails
|
|
|
|
case let .chatCmdError(u, chatError): return withUser(u, String(describing: chatError))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var noDetails: String { "\(responseType): no details" }
|
|
|
|
|
|
|
|
static func chatResponse(_ s: String) -> SEChatResponse {
|
|
|
|
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 {
|
|
|
|
let r = try jsonDecoder.decode(APIResponse<SEChatResponse>.self, from: d)
|
|
|
|
return r.resp
|
|
|
|
} catch {
|
|
|
|
logger.error("chatResponse jsonDecoder.decode error: \(error.localizedDescription)")
|
|
|
|
}
|
|
|
|
|
|
|
|
var type: String?
|
|
|
|
var json: String?
|
|
|
|
if let j = try? JSONSerialization.jsonObject(with: d) as? NSDictionary {
|
|
|
|
if let jResp = j["resp"] as? NSDictionary, jResp.count == 1 || jResp.count == 2 {
|
|
|
|
type = jResp.allKeys[0] as? String
|
|
|
|
if jResp.count == 2 && type == "_owsf" {
|
|
|
|
type = jResp.allKeys[1] as? String
|
|
|
|
}
|
|
|
|
if type == "apiChats" {
|
|
|
|
if let r = parseApiChats(jResp) {
|
|
|
|
return .apiChats(user: r.user, chats: r.chats)
|
|
|
|
}
|
|
|
|
} else if type == "chatCmdError" {
|
|
|
|
if let jError = jResp["chatCmdError"] as? NSDictionary {
|
|
|
|
return .chatCmdError(user_: decodeUser_(jError), chatError: .invalidJSON(json: errorJson(jError) ?? ""))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
json = serializeJSON(j, options: .prettyPrinted)
|
|
|
|
}
|
|
|
|
return SEChatResponse.response(type: type ?? "invalid", json: json ?? s)
|
|
|
|
}
|
|
|
|
|
|
|
|
var chatError: ChatError? {
|
|
|
|
switch self {
|
|
|
|
case let .chatCmdError(_, error): error
|
|
|
|
default: nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var chatErrorType: ChatErrorType? {
|
|
|
|
switch self {
|
|
|
|
case let .chatCmdError(_, .error(error)): error
|
2025-05-04 22:14:36 +01:00
|
|
|
default: nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
enum SEChatEvent: Decodable, Error, ChatEventProtocol {
|
|
|
|
case event(type: String, json: String)
|
|
|
|
case chatSuspended
|
|
|
|
case sndFileProgressXFTP(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, sentSize: Int64, totalSize: Int64)
|
|
|
|
case sndFileCompleteXFTP(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta)
|
|
|
|
case chatItemsStatusesUpdated(user: UserRef, chatItems: [AChatItem])
|
|
|
|
case sndFileError(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, errorMessage: String)
|
|
|
|
case sndFileWarning(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, errorMessage: String)
|
|
|
|
case chatError(user_: UserRef?, chatError: ChatError)
|
|
|
|
|
|
|
|
var eventType: String {
|
|
|
|
switch self {
|
|
|
|
case let .event(type, _): "* \(type)"
|
|
|
|
case .chatSuspended: "chatSuspended"
|
|
|
|
case .sndFileProgressXFTP: "sndFileProgressXFTP"
|
|
|
|
case .sndFileCompleteXFTP: "sndFileCompleteXFTP"
|
|
|
|
case .chatItemsStatusesUpdated: "chatItemsStatusesUpdated"
|
|
|
|
case .sndFileError: "sndFileError"
|
|
|
|
case .sndFileWarning: "sndFileWarning"
|
|
|
|
case .chatError: "chatError"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var details: String {
|
|
|
|
switch self {
|
|
|
|
case let .event(_, json): return json
|
|
|
|
case .chatSuspended: return noDetails
|
|
|
|
case let .sndFileProgressXFTP(u, chatItem, _, sentSize, totalSize): return withUser(u, "chatItem: \(String(describing: chatItem))\nsentSize: \(sentSize)\ntotalSize: \(totalSize)")
|
|
|
|
case let .sndFileCompleteXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
|
|
|
case let .chatItemsStatusesUpdated(u, chatItems):
|
|
|
|
let itemsString = chatItems.map { chatItem in String(describing: chatItem) }.joined(separator: "\n")
|
|
|
|
return withUser(u, itemsString)
|
|
|
|
case let .sndFileError(u, chatItem, _, err): return withUser(u, "error: \(String(describing: err))\nchatItem: \(String(describing: chatItem))")
|
|
|
|
case let .sndFileWarning(u, chatItem, _, err): return withUser(u, "error: \(String(describing: err))\nchatItem: \(String(describing: chatItem))")
|
|
|
|
case let .chatError(u, chatError): return withUser(u, String(describing: chatError))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var noDetails: String { "\(eventType): no details" }
|
|
|
|
|
|
|
|
static func chatEvent(_ s: String) -> SEChatEvent {
|
|
|
|
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 {
|
|
|
|
let r = try jsonDecoder.decode(APIResponse<SEChatEvent>.self, from: d)
|
|
|
|
return r.resp
|
|
|
|
} catch {
|
|
|
|
logger.error("chatResponse jsonDecoder.decode error: \(error.localizedDescription)")
|
|
|
|
}
|
|
|
|
|
|
|
|
var type: String?
|
|
|
|
var json: String?
|
|
|
|
if let j = try? JSONSerialization.jsonObject(with: d) as? NSDictionary {
|
|
|
|
if let jResp = j["resp"] as? NSDictionary, jResp.count == 1 || jResp.count == 2 {
|
|
|
|
type = jResp.allKeys[0] as? String
|
|
|
|
if jResp.count == 2 && type == "_owsf" {
|
|
|
|
type = jResp.allKeys[1] as? String
|
|
|
|
}
|
|
|
|
if type == "chatError" {
|
|
|
|
if let jError = jResp["chatError"] as? NSDictionary {
|
|
|
|
return .chatError(user_: decodeUser_(jError), chatError: .invalidJSON(json: errorJson(jError) ?? ""))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
json = serializeJSON(j, options: .prettyPrinted)
|
|
|
|
}
|
|
|
|
return SEChatEvent.event(type: type ?? "invalid", json: json ?? s)
|
|
|
|
}
|
|
|
|
var chatError: ChatError? {
|
|
|
|
switch self {
|
|
|
|
case let .chatError(_, error): error
|
|
|
|
default: nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var chatErrorType: ChatErrorType? {
|
|
|
|
switch self {
|
2025-05-02 12:27:08 +01:00
|
|
|
case let .chatError(_, .error(error)): error
|
|
|
|
default: nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|