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
7.4 KiB
Server Hooks
The ServerHooks trait provides a plugin system for extending the quicprochat
server. Hooks fire at key points in the request lifecycle — message delivery,
authentication, channel creation, and message fetch — allowing you to inspect,
log, rate-limit, or reject operations without modifying server internals.
Overview
Client RPC request
└─ Validation (auth, rate limits, wire format)
└─ Hook fires (on_message_enqueue, on_auth, etc.)
├─ HookAction::Continue → proceed to storage/delivery
└─ HookAction::Reject("reason") → error returned to client
Hooks are called synchronously in the RPC handler path after validation but before storage. Keep hook implementations fast — offload heavy work (HTTP calls, disk I/O, analytics) to background tasks.
The ServerHooks trait
pub trait ServerHooks: Send + Sync {
/// Called before a message is stored in the delivery queue.
/// Return HookAction::Reject to prevent delivery.
fn on_message_enqueue(&self, event: &MessageEvent) -> HookAction {
HookAction::Continue
}
/// Called after a batch of messages is enqueued.
fn on_batch_enqueue(&self, events: &[MessageEvent]) {}
/// Called after a successful or failed login attempt.
fn on_auth(&self, event: &AuthEvent) {}
/// Called after a channel is created or looked up.
fn on_channel_created(&self, event: &ChannelEvent) {}
/// Called after messages are fetched from the delivery queue.
fn on_fetch(&self, event: &FetchEvent) {}
/// Called when a user completes OPAQUE registration.
fn on_user_registered(&self, username: &str, identity_key: &[u8]) {}
}
All methods have default no-op implementations. Override only the events you care about.
Hook action
pub enum HookAction {
/// Allow the operation to proceed.
Continue,
/// Reject the operation with a reason (returned to the client as an error).
Reject(String),
}
Currently only on_message_enqueue can reject operations. Other hooks are
observational (fire-and-forget).
Event types
MessageEvent
Fired on enqueue and batch_enqueue RPC calls.
| Field | Type | Description |
|---|---|---|
sender_identity |
Option<Vec<u8>> |
Sender's 32-byte identity key (None in sealed sender mode). |
recipient_key |
Vec<u8> |
Recipient's 32-byte identity key. |
channel_id |
Vec<u8> |
16-byte channel ID. |
payload_len |
usize |
Length of the encrypted payload in bytes. |
seq |
u64 |
Server-assigned sequence number. |
AuthEvent
Fired after OPAQUE login completes (success or failure).
| Field | Type | Description |
|---|---|---|
username |
String |
The username that attempted to authenticate. |
success |
bool |
Whether authentication succeeded. |
failure_reason |
String |
Failure reason (empty on success). |
ChannelEvent
Fired after a createChannel RPC call.
| Field | Type | Description |
|---|---|---|
channel_id |
Vec<u8> |
16-byte channel ID. |
initiator_key |
Vec<u8> |
Identity key of the channel initiator. |
peer_key |
Vec<u8> |
Identity key of the peer. |
was_new |
bool |
True if this is a newly created channel. |
FetchEvent
Fired after a fetch or fetchWait RPC call.
| Field | Type | Description |
|---|---|---|
recipient_key |
Vec<u8> |
Identity key of the fetcher. |
channel_id |
Vec<u8> |
Channel ID being fetched from. |
message_count |
usize |
Number of messages returned. |
Built-in implementations
NoopHooks
Does nothing. This is the default when no hooks are configured.
pub struct NoopHooks;
impl ServerHooks for NoopHooks {}
TracingHooks
Logs all events via the tracing crate at info/debug level.
pub struct TracingHooks;
impl ServerHooks for TracingHooks {
fn on_message_enqueue(&self, event: &MessageEvent) -> HookAction {
tracing::info!(
recipient_prefix = %hex_prefix(&event.recipient_key),
payload_len = event.payload_len,
seq = event.seq,
"hook: message enqueued"
);
HookAction::Continue
}
fn on_auth(&self, event: &AuthEvent) {
if event.success {
tracing::info!(username = %event.username, "hook: login success");
} else {
tracing::warn!(
username = %event.username,
reason = %event.failure_reason,
"hook: login failure"
);
}
}
// ... other methods log similarly
}
Writing a custom hook
Example: payload size limiter
use quicprochat_server::hooks::{ServerHooks, HookAction, MessageEvent};
struct PayloadLimiter {
max_bytes: usize,
}
impl ServerHooks for PayloadLimiter {
fn on_message_enqueue(&self, event: &MessageEvent) -> HookAction {
if event.payload_len > self.max_bytes {
return HookAction::Reject(format!(
"payload too large: {} > {} bytes",
event.payload_len, self.max_bytes
));
}
HookAction::Continue
}
}
Example: login auditor
use quicprochat_server::hooks::{ServerHooks, AuthEvent};
struct LoginAuditor;
impl ServerHooks for LoginAuditor {
fn on_auth(&self, event: &AuthEvent) {
if !event.success {
eprintln!(
"AUDIT: failed login for '{}': {}",
event.username, event.failure_reason
);
}
}
}
Example: composing multiple hooks
use quicprochat_server::hooks::*;
struct CompositeHooks {
hooks: Vec<Box<dyn ServerHooks>>,
}
impl ServerHooks for CompositeHooks {
fn on_message_enqueue(&self, event: &MessageEvent) -> HookAction {
for hook in &self.hooks {
if let HookAction::Reject(reason) = hook.on_message_enqueue(event) {
return HookAction::Reject(reason);
}
}
HookAction::Continue
}
fn on_auth(&self, event: &AuthEvent) {
for hook in &self.hooks {
hook.on_auth(event);
}
}
// ... delegate other methods similarly
}
Important considerations
- E2E encryption: Message payloads are encrypted end-to-end. Hooks cannot inspect plaintext content — they see only metadata (sender, recipient, payload size, sequence number).
- Performance: Hooks run synchronously in the RPC handler. A slow hook
blocks the RPC response. Use
tokio::spawnfor async work. - Thread safety:
ServerHooksrequiresSend + Sync. UseArc<Mutex<_>>or lock-free structures for shared mutable state. - Reject semantics: Only
on_message_enqueuesupports rejection. Other hooks are informational — the operation proceeds regardless of what the hook does.
Further reading
- Delivery Service Internals -- how messages flow through the server
- Authentication Service Internals -- OPAQUE auth flow
- Bot SDK -- build bots that interact with the server