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.
This commit is contained in:
2026-03-07 18:24:52 +01:00
parent d8c1392587
commit a710037dde
212 changed files with 609 additions and 609 deletions

View File

@@ -0,0 +1,348 @@
//! `QpqClient` — the main entry point for the quicprochat SDK.
use std::sync::Arc;
use tokio::sync::broadcast;
use tracing::info;
use crate::config::ClientConfig;
use crate::conversation::ConversationStore;
use crate::error::SdkError;
use crate::events::ClientEvent;
/// The main SDK client. All state is contained within this struct — no globals.
pub struct QpqClient {
config: ClientConfig,
rpc: Option<quicprochat_rpc::client::RpcClient>,
event_tx: broadcast::Sender<ClientEvent>,
/// The authenticated username, if logged in.
username: Option<String>,
/// The local identity key (Ed25519 public key, 32 bytes).
identity_key: Option<Vec<u8>>,
/// Session token from OPAQUE login.
session_token: Option<Vec<u8>>,
/// Local conversation store (SQLCipher).
conv_store: Option<ConversationStore>,
/// Device ID for multi-device support.
/// When set, fetch/peek/ack requests include this device_id so the server
/// scopes them to the correct per-device queue.
device_id: Option<Vec<u8>>,
}
impl QpqClient {
/// Create a new client with the given configuration.
pub fn new(config: ClientConfig) -> Self {
let (event_tx, _) = broadcast::channel(256);
Self {
config,
rpc: None,
event_tx,
username: None,
identity_key: None,
session_token: None,
conv_store: None,
device_id: None,
}
}
/// Connect to the server.
pub async fn connect(&mut self) -> Result<(), SdkError> {
let tls_config = build_tls_config(self.config.accept_invalid_certs)?;
let rpc_config = quicprochat_rpc::client::RpcClientConfig {
server_addr: self.config.server_addr,
server_name: self.config.server_name.clone(),
tls_config: Arc::new(tls_config),
alpn: self.config.alpn.clone(),
session_token: self.session_token.clone(),
};
let client = quicprochat_rpc::client::RpcClient::connect(rpc_config).await?;
self.rpc = Some(client);
// Open local conversation store.
let store = ConversationStore::open(
&self.config.db_path,
self.config.db_password.as_deref(),
)?;
self.conv_store = Some(store);
self.emit(ClientEvent::Connected);
info!(server = %self.config.server_addr, "connected");
Ok(())
}
/// Subscribe to client events. Returns a broadcast receiver.
pub fn subscribe(&self) -> broadcast::Receiver<ClientEvent> {
self.event_tx.subscribe()
}
/// Get the authenticated username, if logged in.
pub fn username(&self) -> Option<&str> {
self.username.as_deref()
}
/// Get the local identity key.
pub fn identity_key(&self) -> Option<&[u8]> {
self.identity_key.as_deref()
}
/// Whether the client is connected.
pub fn is_connected(&self) -> bool {
self.rpc.is_some()
}
/// Whether the client is authenticated.
pub fn is_authenticated(&self) -> bool {
self.session_token.is_some()
}
/// Get the server address as a string (e.g. "127.0.0.1:7000").
pub fn server_addr_string(&self) -> String {
self.config.server_addr.to_string()
}
/// Get the state file path from the client configuration.
pub fn config_state_path(&self) -> std::path::PathBuf {
self.config.state_path.clone()
}
/// Get a reference to the RPC client (for direct calls).
pub fn rpc(&self) -> Result<&quicprochat_rpc::client::RpcClient, SdkError> {
self.rpc.as_ref().ok_or(SdkError::NotConnected)
}
/// Get a reference to the conversation store.
pub fn conversations(&self) -> Result<&ConversationStore, SdkError> {
self.conv_store
.as_ref()
.ok_or(SdkError::NotConnected)
}
/// Register a new user account via OPAQUE.
///
/// Generates a fresh identity keypair, registers it with the server, and
/// stores the identity key locally.
pub async fn register(&mut self, username: &str, password: &str) -> Result<(), SdkError> {
let rpc = self.rpc.as_ref().ok_or(SdkError::NotConnected)?;
let keypair = crate::auth::opaque_register(rpc, username, password, None).await?;
self.identity_key = Some(keypair.public_key_bytes().to_vec());
self.emit(ClientEvent::Registered {
username: username.to_string(),
});
info!(username, "registered");
Ok(())
}
/// Log in via OPAQUE and store the session token.
///
/// Requires an identity key to be set (either from a previous `register()`
/// call or loaded from state). After login, the client is authenticated
/// and subsequent RPC calls include the session token.
pub async fn login(&mut self, username: &str, password: &str) -> Result<(), SdkError> {
let identity_key = self
.identity_key
.as_ref()
.ok_or_else(|| SdkError::AuthFailed("no identity key — register or load state first".into()))?
.clone();
let rpc = self.rpc.as_ref().ok_or(SdkError::NotConnected)?;
let session_token = crate::auth::opaque_login(rpc, username, password, &identity_key).await?;
self.session_token = Some(session_token);
self.username = Some(username.to_string());
self.emit(ClientEvent::LoggedIn {
username: username.to_string(),
});
info!(username, "logged in");
Ok(())
}
/// Clear authentication state (session token, username).
pub fn logout(&mut self) -> Result<(), SdkError> {
self.session_token = None;
let username = self.username.take();
self.emit(ClientEvent::LoggedOut {
username: username.unwrap_or_default(),
});
info!("logged out");
Ok(())
}
/// Set the identity key directly (e.g. after loading from state).
pub fn set_identity_key(&mut self, key: Vec<u8>) {
self.identity_key = Some(key);
}
/// Get the session token, if authenticated.
pub fn session_token(&self) -> Option<&[u8]> {
self.session_token.as_deref()
}
// ── Multi-device ─────────────────────────────────────────────────────────
/// Set the device ID for this client. Subsequent fetch/peek/ack calls
/// will include this ID so the server scopes them to the correct queue.
pub fn set_device_id(&mut self, device_id: Vec<u8>) {
self.device_id = Some(device_id);
}
/// Get the current device ID, if set.
pub fn device_id(&self) -> Option<&[u8]> {
self.device_id.as_deref()
}
/// Register this device with the server.
/// Sets the local device_id on success.
pub async fn register_device(
&mut self,
device_id: &[u8],
device_name: &str,
) -> Result<bool, SdkError> {
let rpc = self.rpc.as_ref().ok_or(SdkError::NotConnected)?;
let newly_registered =
crate::devices::register_device(rpc, device_id, device_name).await?;
self.device_id = Some(device_id.to_vec());
Ok(newly_registered)
}
/// List all registered devices for this identity.
pub async fn list_devices(&self) -> Result<Vec<crate::devices::DeviceInfo>, SdkError> {
let rpc = self.rpc.as_ref().ok_or(SdkError::NotConnected)?;
crate::devices::list_devices(rpc).await
}
/// Revoke (remove) a registered device.
pub async fn revoke_device(&self, device_id: &[u8]) -> Result<bool, SdkError> {
let rpc = self.rpc.as_ref().ok_or(SdkError::NotConnected)?;
crate::devices::revoke_device(rpc, device_id).await
}
// ── Moderation (client-side) ────────────────────────────────────────────
/// Block a user locally. Their messages will be hidden from display.
pub fn block_user(&self, identity_key: &[u8], reason: &str) -> Result<(), SdkError> {
let store = self.conversations()?;
store
.block_user(identity_key, reason)
.map_err(|e| SdkError::Storage(e.to_string()))?;
info!(identity = %hex::encode(identity_key), "user blocked");
Ok(())
}
/// Unblock a user locally.
pub fn unblock_user(&self, identity_key: &[u8]) -> Result<bool, SdkError> {
let store = self.conversations()?;
let removed = store
.unblock_user(identity_key)
.map_err(|e| SdkError::Storage(e.to_string()))?;
if removed {
info!(identity = %hex::encode(identity_key), "user unblocked");
}
Ok(removed)
}
/// Check if a user is blocked locally.
pub fn is_blocked(&self, identity_key: &[u8]) -> Result<bool, SdkError> {
let store = self.conversations()?;
store
.is_blocked(identity_key)
.map_err(|e| SdkError::Storage(e.to_string()))
}
/// List all locally blocked users.
pub fn list_blocked(&self) -> Result<Vec<crate::conversation::BlockedUser>, SdkError> {
let store = self.conversations()?;
store
.list_blocked()
.map_err(|e| SdkError::Storage(e.to_string()))
}
/// Disconnect from the server.
pub fn disconnect(&mut self) {
if let Some(rpc) = self.rpc.take() {
rpc.close();
self.emit(ClientEvent::Disconnected {
reason: "client closed".into(),
});
}
}
fn emit(&self, event: ClientEvent) {
// Ignore send errors (no subscribers).
let _ = self.event_tx.send(event);
}
}
impl Drop for QpqClient {
fn drop(&mut self) {
self.disconnect();
}
}
fn build_tls_config(accept_invalid_certs: bool) -> Result<rustls::ClientConfig, SdkError> {
let builder = rustls::ClientConfig::builder();
if accept_invalid_certs {
let config = builder
.dangerous()
.with_custom_certificate_verifier(Arc::new(InsecureVerifier))
.with_no_client_auth();
Ok(config)
} else {
let roots = rustls::RootCertStore::empty();
let config = builder
.with_root_certificates(roots)
.with_no_client_auth();
Ok(config)
}
}
/// A TLS verifier that accepts any certificate (for dev mode only).
#[derive(Debug)]
struct InsecureVerifier;
impl rustls::client::danger::ServerCertVerifier for InsecureVerifier {
fn verify_server_cert(
&self,
_end_entity: &rustls::pki_types::CertificateDer<'_>,
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
_server_name: &rustls::pki_types::ServerName<'_>,
_ocsp_response: &[u8],
_now: rustls::pki_types::UnixTime,
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
Ok(rustls::client::danger::ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
vec![
rustls::SignatureScheme::RSA_PKCS1_SHA256,
rustls::SignatureScheme::RSA_PKCS1_SHA384,
rustls::SignatureScheme::RSA_PKCS1_SHA512,
rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
rustls::SignatureScheme::ED25519,
rustls::SignatureScheme::RSA_PSS_SHA256,
rustls::SignatureScheme::RSA_PSS_SHA384,
rustls::SignatureScheme::RSA_PSS_SHA512,
]
}
}