chore: rename quicproquo → quicprochat in docs, Docker, CI, and packaging
Rename all project references from quicproquo/qpq to quicprochat/qpc across documentation, Docker configuration, CI workflows, packaging scripts, operational configs, and build tooling. - Docker: crate paths, binary names, user/group, data dirs, env vars - CI: workflow crate references, binary names, artifact names - Docs: all markdown files under docs/, SDK READMEs, book.toml - Packaging: OpenWrt Makefile, init script, UCI config (file renames) - Scripts: justfile, dev-shell, screenshot, cross-compile, ai_team - Operations: Prometheus config, alert rules, Grafana dashboard - Config: .env.example (QPQ_* → QPC_*), CODEOWNERS paths - Top-level: README, CONTRIBUTING, ROADMAP, CLAUDE.md
This commit is contained in:
155
sdks/swift/Sources/QuicProChat/QpqClient.swift
Normal file
155
sdks/swift/Sources/QuicProChat/QpqClient.swift
Normal file
@@ -0,0 +1,155 @@
|
||||
import Foundation
|
||||
import CQuicProChat
|
||||
|
||||
/// High-level quicprochat client for iOS/macOS.
|
||||
///
|
||||
/// Wraps ``libquicprochat_ffi`` to provide a Swift-native API for
|
||||
/// connecting, authenticating, and messaging.
|
||||
///
|
||||
/// ```swift
|
||||
/// let client = try QpqClient(
|
||||
/// server: "127.0.0.1:5001",
|
||||
/// caCertPath: "/path/to/ca.pem"
|
||||
/// )
|
||||
/// try client.login(username: "alice", password: "secret")
|
||||
/// try client.send(to: "bob", message: "hello".data(using: .utf8)!)
|
||||
/// let messages = try client.receive(timeoutMs: 5000)
|
||||
/// client.disconnect()
|
||||
/// ```
|
||||
public final class QpqClient: @unchecked Sendable {
|
||||
private var handle: OpaquePointer?
|
||||
private let lock = NSLock()
|
||||
|
||||
/// Connect to a quicprochat server.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - server: Server address as ``host:port``.
|
||||
/// - caCertPath: Path to the PEM-encoded CA certificate.
|
||||
/// - serverName: TLS SNI server name (defaults to host from *server*).
|
||||
/// - Throws: ``QpqError/connectionFailed(_:)`` if the connection fails.
|
||||
public init(server: String, caCertPath: String, serverName: String? = nil) throws {
|
||||
let sn = serverName ?? server.components(separatedBy: ":").first ?? server
|
||||
|
||||
let h = qpq_connect(server, caCertPath, sn)
|
||||
guard let h else {
|
||||
throw QpqError.connectionFailed("qpq_connect returned NULL for \(server)")
|
||||
}
|
||||
self.handle = h
|
||||
}
|
||||
|
||||
deinit {
|
||||
disconnect()
|
||||
}
|
||||
|
||||
// MARK: - Authentication
|
||||
|
||||
/// Authenticate with the server using OPAQUE (username + password).
|
||||
///
|
||||
/// - Throws: ``QpqError/authFailed(_:)`` on bad credentials.
|
||||
public func login(username: String, password: String) throws {
|
||||
try withHandle { h in
|
||||
let code = qpq_login(h, username, password)
|
||||
try checkStatus(code, handle: h)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Messaging
|
||||
|
||||
/// Send a message to a recipient by username.
|
||||
///
|
||||
/// The message is encrypted via MLS before delivery.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - recipient: Recipient username.
|
||||
/// - message: Message payload (arbitrary bytes).
|
||||
public func send(to recipient: String, message: Data) throws {
|
||||
try withHandle { h in
|
||||
let code = message.withUnsafeBytes { buffer -> Int32 in
|
||||
guard let ptr = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return QPQ_ERROR
|
||||
}
|
||||
return qpq_send(h, recipient, ptr, buffer.count)
|
||||
}
|
||||
try checkStatus(code, handle: h)
|
||||
}
|
||||
}
|
||||
|
||||
/// Receive pending messages, blocking up to ``timeoutMs`` milliseconds.
|
||||
///
|
||||
/// - Returns: An array of message strings (UTF-8).
|
||||
public func receive(timeoutMs: UInt32 = 5000) throws -> [String] {
|
||||
try withHandle { h in
|
||||
var jsonPtr: UnsafeMutablePointer<CChar>?
|
||||
let code = qpq_receive(h, timeoutMs, &jsonPtr)
|
||||
try checkStatus(code, handle: h)
|
||||
|
||||
guard let jsonPtr else {
|
||||
return []
|
||||
}
|
||||
defer { qpq_free_string(jsonPtr) }
|
||||
|
||||
let jsonString = String(cString: jsonPtr)
|
||||
|
||||
guard let data = jsonString.data(using: .utf8),
|
||||
let array = try? JSONSerialization.jsonObject(with: data) as? [String]
|
||||
else {
|
||||
return []
|
||||
}
|
||||
return array
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
/// Disconnect from the server and release resources.
|
||||
public func disconnect() {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
|
||||
if let h = handle {
|
||||
qpq_disconnect(h)
|
||||
handle = nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the client is currently connected.
|
||||
public var isConnected: Bool {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
return handle != nil
|
||||
}
|
||||
|
||||
// MARK: - Internal
|
||||
|
||||
private func withHandle<T>(_ body: (OpaquePointer) throws -> T) throws -> T {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
|
||||
guard let h = handle else {
|
||||
throw QpqError.notConnected
|
||||
}
|
||||
return try body(h)
|
||||
}
|
||||
|
||||
private func checkStatus(_ code: Int32, handle: OpaquePointer) throws {
|
||||
guard code != QPQ_OK else { return }
|
||||
|
||||
let msg: String
|
||||
if let errPtr = qpq_last_error(handle) {
|
||||
msg = String(cString: errPtr)
|
||||
} else {
|
||||
msg = "unknown error"
|
||||
}
|
||||
|
||||
switch code {
|
||||
case QPQ_AUTH_FAILED:
|
||||
throw QpqError.authFailed(msg)
|
||||
case QPQ_TIMEOUT:
|
||||
throw QpqError.timeout(msg)
|
||||
case QPQ_NOT_CONNECTED:
|
||||
throw QpqError.notConnected
|
||||
default:
|
||||
throw QpqError.ffiError(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
23
sdks/swift/Sources/QuicProChat/QpqError.swift
Normal file
23
sdks/swift/Sources/QuicProChat/QpqError.swift
Normal file
@@ -0,0 +1,23 @@
|
||||
/// Errors returned by the QuicProChat SDK.
|
||||
public enum QpqError: Error, Sendable, CustomStringConvertible {
|
||||
/// Connection to the server failed.
|
||||
case connectionFailed(String)
|
||||
/// OPAQUE authentication failed (bad credentials).
|
||||
case authFailed(String)
|
||||
/// The operation timed out.
|
||||
case timeout(String)
|
||||
/// The client is not connected.
|
||||
case notConnected
|
||||
/// A generic error from the FFI layer.
|
||||
case ffiError(String)
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .connectionFailed(let msg): return "connection failed: \(msg)"
|
||||
case .authFailed(let msg): return "auth failed: \(msg)"
|
||||
case .timeout(let msg): return "timeout: \(msg)"
|
||||
case .notConnected: return "not connected"
|
||||
case .ffiError(let msg): return "FFI error: \(msg)"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user