2025-08-24 16:24:21 -07:00
|
|
|
//
|
|
|
|
|
// XPCClient.swift
|
|
|
|
|
// kordophone2
|
|
|
|
|
//
|
|
|
|
|
// Created by James Magahern on 8/24/25.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
|
import XPC
|
|
|
|
|
|
|
|
|
|
private let serviceName = "net.buzzert.kordophonecd"
|
|
|
|
|
|
|
|
|
|
final class XPCClient
|
|
|
|
|
{
|
|
|
|
|
private let connection: xpc_connection_t
|
|
|
|
|
|
|
|
|
|
init() {
|
|
|
|
|
self.connection = xpc_connection_create_mach_service(serviceName, nil, 0)
|
|
|
|
|
|
|
|
|
|
let handler: xpc_handler_t = { _ in }
|
|
|
|
|
xpc_connection_set_event_handler(connection, handler)
|
|
|
|
|
xpc_connection_resume(connection)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func getVersion() async throws -> String {
|
|
|
|
|
let req = makeRequest(method: "GetVersion")
|
|
|
|
|
guard let reply = try await sendSync(req), xpc_get_type(reply) == XPC_TYPE_DICTIONARY else { throw Error.typeError }
|
|
|
|
|
if let cstr = xpc_dictionary_get_string(reply, "version") {
|
|
|
|
|
return String(cString: cstr)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw Error.typeError
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func getConversations(limit: Int = 100, offset: Int = 0) async throws -> [Serialized.Conversation] {
|
|
|
|
|
var args: [String: xpc_object_t] = [:]
|
|
|
|
|
args["limit"] = xpcString(String(limit))
|
|
|
|
|
args["offset"] = xpcString(String(offset))
|
|
|
|
|
|
|
|
|
|
let req = makeRequest(method: "GetConversations", arguments: args)
|
|
|
|
|
guard let reply = try await sendSync(req), xpc_get_type(reply) == XPC_TYPE_DICTIONARY else { return [] }
|
|
|
|
|
guard let items = xpc_dictionary_get_value(reply, "conversations"), xpc_get_type(items) == XPC_TYPE_ARRAY else { return [] }
|
|
|
|
|
|
|
|
|
|
var results: [Serialized.Conversation] = []
|
|
|
|
|
xpc_array_apply(items) { _, element in
|
|
|
|
|
if xpc_get_type(element) == XPC_TYPE_DICTIONARY, let conv = Serialized.Conversation(xpc: element) {
|
|
|
|
|
results.append(conv)
|
|
|
|
|
}
|
2025-08-24 17:58:37 -07:00
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return results
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func getMessages(conversationId: String, limit: Int = 100, offset: Int = 0) async throws -> [Serialized.Message] {
|
|
|
|
|
var args: [String: xpc_object_t] = [:]
|
|
|
|
|
args["conversation_id"] = xpcString(conversationId)
|
|
|
|
|
args["limit"] = xpcString(String(limit))
|
|
|
|
|
args["offset"] = xpcString(String(offset))
|
|
|
|
|
|
|
|
|
|
let req = makeRequest(method: "GetMessages", arguments: args)
|
|
|
|
|
guard let reply = try await sendSync(req), xpc_get_type(reply) == XPC_TYPE_DICTIONARY else { return [] }
|
|
|
|
|
guard let items = xpc_dictionary_get_value(reply, "messages"), xpc_get_type(items) == XPC_TYPE_ARRAY else { return [] }
|
|
|
|
|
|
|
|
|
|
var results: [Serialized.Message] = []
|
|
|
|
|
xpc_array_apply(items) { _, element in
|
|
|
|
|
if xpc_get_type(element) == XPC_TYPE_DICTIONARY, let msg = Serialized.Message(xpc: element) {
|
|
|
|
|
results.append(msg)
|
|
|
|
|
}
|
2025-08-24 16:24:21 -07:00
|
|
|
return true
|
|
|
|
|
}
|
2025-08-24 17:58:37 -07:00
|
|
|
|
2025-08-24 16:24:21 -07:00
|
|
|
return results
|
|
|
|
|
}
|
2025-08-24 17:58:37 -07:00
|
|
|
|
|
|
|
|
public func sendMessage(conversationId: String, message: String) async throws {
|
|
|
|
|
var args: [String: xpc_object_t] = [:]
|
|
|
|
|
args["conversation_id"] = xpcString(conversationId)
|
|
|
|
|
args["text"] = xpcString(message)
|
|
|
|
|
|
|
|
|
|
let req = makeRequest(method: "SendMessage", arguments: args)
|
|
|
|
|
guard let reply = try await sendSync(req), xpc_get_type(reply) == XPC_TYPE_DICTIONARY else { throw Error.typeError }
|
|
|
|
|
}
|
2025-08-24 16:24:21 -07:00
|
|
|
|
|
|
|
|
// MARK: - Types
|
|
|
|
|
|
|
|
|
|
enum Error: Swift.Error
|
|
|
|
|
{
|
|
|
|
|
case typeError
|
|
|
|
|
case encodingError
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
extension XPCClient
|
|
|
|
|
{
|
|
|
|
|
private func makeRequest(method: String, arguments: [String: xpc_object_t]? = nil) -> xpc_object_t {
|
|
|
|
|
let dict = xpc_dictionary_create(nil, nil, 0)
|
|
|
|
|
xpc_dictionary_set_string(dict, "method", method)
|
|
|
|
|
if let args = arguments {
|
|
|
|
|
let argsDict = xpc_dictionary_create(nil, nil, 0)
|
|
|
|
|
for (k, v) in args {
|
|
|
|
|
k.withCString { cKey in
|
|
|
|
|
xpc_dictionary_set_value(argsDict, cKey, v)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
xpc_dictionary_set_value(dict, "arguments", argsDict)
|
|
|
|
|
}
|
|
|
|
|
return dict
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func xpcString(_ s: String) -> xpc_object_t {
|
|
|
|
|
return s.withCString { ptr in xpc_string_create(ptr) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func sendSync(_ request: xpc_object_t) async throws -> xpc_object_t? {
|
|
|
|
|
try await withCheckedThrowingContinuation { continuation in
|
|
|
|
|
xpc_connection_send_message_with_reply(connection, request, DispatchQueue.global(qos: .userInitiated)) { r in
|
|
|
|
|
switch xpc_get_type(r) {
|
|
|
|
|
case XPC_TYPE_ERROR:
|
|
|
|
|
let error = xpc_dictionary_get_value(r, "error")
|
|
|
|
|
if let error = error, let errorString = xpc_string_get_string_ptr(error) {
|
|
|
|
|
print("XPC error: \(String(cString: errorString))")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
continuation.resume(throwing: Error.typeError)
|
|
|
|
|
|
|
|
|
|
case XPC_TYPE_DICTIONARY:
|
|
|
|
|
continuation.resume(returning: r)
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
continuation.resume(throwing: Error.typeError)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|