Files
quicproquo/sdks/ruby/lib/quicprochat/client.rb
Christian Nennemann a710037dde chore: rename quicproquo → quicprochat in Rust workspace
Rename all crate directories, package names, binary names, proto
package/module paths, ALPN strings, env var prefixes, config filenames,
mDNS service names, and plugin ABI symbols from quicproquo/qpq to
quicprochat/qpc.
2026-03-21 19:14:06 +01:00

123 lines
3.6 KiB
Ruby

# frozen_string_literal: true
require "json"
module QuicProQuo
# High-level quicproquo client for Ruby.
#
# Wraps +libquicproquo_ffi+ via the +ffi+ gem.
#
# client = QuicProQuo::Client.new("127.0.0.1:5001", ca_cert: "ca.pem")
# client.login("alice", "secret")
# client.send("bob", "hello")
# messages = client.receive(timeout_ms: 5000)
# client.disconnect
#
# Or use the block form for automatic cleanup:
#
# QuicProQuo::Client.open("127.0.0.1:5001", ca_cert: "ca.pem") do |c|
# c.login("alice", "secret")
# c.send("bob", "hello")
# end
class Client
# Connect to a quicproquo server.
#
# @param server [String] Server address as +host:port+.
# @param ca_cert [String] Path to PEM-encoded CA certificate.
# @param server_name [String] TLS SNI server name (defaults to host).
# @raise [ConnectionError] if the connection fails.
def initialize(server, ca_cert:, server_name: nil)
sn = server_name || server.split(":").first
@handle = FFIBindings.qpq_connect(server, ca_cert, sn)
raise ConnectionError, "qpq_connect failed for #{server}" if @handle.null?
end
# Connect and yield the client, disconnecting when the block returns.
def self.open(server, **opts)
client = new(server, **opts)
begin
yield client
ensure
client.disconnect
end
end
# Authenticate with OPAQUE credentials.
#
# @param username [String]
# @param password [String]
# @raise [AuthError] on bad credentials.
def login(username, password)
check_connected!
code = FFIBindings.qpq_login(@handle, username, password)
check_status!(code)
end
# Send a message to a recipient by username.
#
# @param recipient [String] Recipient username.
# @param message [String] Message payload (UTF-8 string or binary).
def send(recipient, message)
check_connected!
msg_bytes = message.encode("BINARY")
buf = FFI::MemoryPointer.new(:uint8, msg_bytes.bytesize)
buf.put_bytes(0, msg_bytes)
code = FFIBindings.qpq_send(@handle, recipient, buf, msg_bytes.bytesize)
check_status!(code)
end
# Receive pending messages, blocking up to +timeout_ms+ milliseconds.
#
# @param timeout_ms [Integer] Timeout in milliseconds (default 5000).
# @return [Array<String>] Message strings (UTF-8).
def receive(timeout_ms: 5000)
check_connected!
out_ptr = FFI::MemoryPointer.new(:pointer)
code = FFIBindings.qpq_receive(@handle, timeout_ms, out_ptr)
check_status!(code)
json_ptr = out_ptr.read_pointer
return [] if json_ptr.null?
json_str = json_ptr.read_string
FFIBindings.qpq_free_string(json_ptr)
JSON.parse(json_str)
end
# Disconnect from the server and release resources.
def disconnect
return if @handle.nil? || @handle.null?
FFIBindings.qpq_disconnect(@handle)
@handle = nil
end
# Whether the client is connected.
def connected?
!@handle.nil? && !@handle.null?
end
private
def check_connected!
raise NotConnectedError, "not connected" unless connected?
end
def check_status!(code)
return if code == FFIBindings::QPQ_OK
msg = FFIBindings.qpq_last_error(@handle) || "unknown error"
case code
when FFIBindings::QPQ_AUTH_FAILED
raise AuthError, msg
when FFIBindings::QPQ_TIMEOUT
raise TimeoutError, msg
when FFIBindings::QPQ_NOT_CONNECTED
raise NotConnectedError, msg
else
raise Error, msg
end
end
end
end