//! Server-side plugin hooks for extending quicproquo. //! //! Implement the [`ServerHooks`] trait to intercept server events — message delivery, //! authentication, channel creation, and more. Hooks fire after validation but before //! storage, so they can inspect, log, or reject operations. //! //! # Built-in implementations //! //! - [`NoopHooks`] — does nothing (default when no hooks are configured) //! - [`TracingHooks`] — logs all events via `tracing` at info/debug level //! //! # Writing a custom hook //! //! ```rust,ignore //! use quicproquo_server::hooks::{ServerHooks, HookAction, MessageEvent}; //! //! struct ModeratorHook { //! banned_words: Vec, //! } //! //! impl ServerHooks for ModeratorHook { //! fn on_message_enqueue(&self, event: &MessageEvent) -> HookAction { //! // Can't inspect encrypted content (E2E), but can enforce rate limits, //! // payload size limits, or sender restrictions. //! if event.payload_len > 1_000_000 { //! return HookAction::Reject("payload too large".into()); //! } //! HookAction::Continue //! } //! } //! ``` /// The result of a hook invocation. #[derive(Clone, Debug)] pub enum HookAction { /// Allow the operation to proceed. Continue, /// Reject the operation with a reason (returned to the client as an error). Reject(String), } /// Event data for message enqueue operations. #[derive(Clone, Debug)] pub struct MessageEvent { /// Sender's identity key (32 bytes), if known (None in sealed sender mode). pub sender_identity: Option>, /// Recipient's identity key (32 bytes). pub recipient_key: Vec, /// Channel ID (16 bytes) if this is a DM channel message. pub channel_id: Vec, /// Length of the encrypted payload in bytes. pub payload_len: usize, /// Server-assigned sequence number. pub seq: u64, } /// Event data for authentication operations. #[derive(Clone, Debug)] pub struct AuthEvent { /// The username attempting to authenticate. pub username: String, /// Whether the authentication succeeded. pub success: bool, /// Failure reason (empty on success). pub failure_reason: String, } /// Event data for channel creation operations. #[derive(Clone, Debug)] pub struct ChannelEvent { /// The channel's unique ID (16 bytes). pub channel_id: Vec, /// Identity key of the initiator. pub initiator_key: Vec, /// Identity key of the peer. pub peer_key: Vec, /// True if this is a newly created channel (initiator creates the MLS group). pub was_new: bool, } /// Event data for message fetch operations. #[derive(Clone, Debug)] pub struct FetchEvent { /// Identity key of the fetcher. pub recipient_key: Vec, /// Channel ID being fetched from. pub channel_id: Vec, /// Number of messages returned. pub message_count: usize, } /// Trait for server-side plugin hooks. /// /// All methods have default implementations that return [`HookAction::Continue`], /// so you only need to override the events you care about. /// /// Hooks are called synchronously in the RPC handler path. Keep them fast — /// offload heavy work (HTTP calls, disk I/O) to background tasks. pub trait ServerHooks: Send + Sync { /// Called after validation, 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]) { // Default: no-op } /// Called after a successful or failed login attempt. fn on_auth(&self, _event: &AuthEvent) { // Default: no-op } /// Called after a channel is created or looked up. fn on_channel_created(&self, _event: &ChannelEvent) { // Default: no-op } /// Called after messages are fetched from the delivery queue. fn on_fetch(&self, _event: &FetchEvent) { // Default: no-op } /// Called when a user registers (OPAQUE registration complete). fn on_user_registered(&self, _username: &str, _identity_key: &[u8]) { // Default: no-op } } /// No-op hook implementation (default). pub struct NoopHooks; impl ServerHooks for NoopHooks {} /// Hook implementation that logs all events via `tracing`. 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, has_sender = event.sender_identity.is_some(), "hook: message enqueued" ); HookAction::Continue } fn on_batch_enqueue(&self, events: &[MessageEvent]) { tracing::info!( count = events.len(), "hook: batch enqueue" ); } 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" ); } } fn on_channel_created(&self, event: &ChannelEvent) { tracing::info!( channel_id = %hex_prefix(&event.channel_id), was_new = event.was_new, "hook: channel created" ); } fn on_fetch(&self, event: &FetchEvent) { if event.message_count > 0 { tracing::debug!( recipient_prefix = %hex_prefix(&event.recipient_key), count = event.message_count, "hook: messages fetched" ); } } fn on_user_registered(&self, username: &str, _identity_key: &[u8]) { tracing::info!(username = %username, "hook: user registered"); } } fn hex_prefix(bytes: &[u8]) -> String { let n = bytes.len().min(4); hex::encode(&bytes[..n]) }