//! Reference quicprochat server plugin: logs all hook events to stderr. //! //! This plugin demonstrates every hook point in the `HookVTable` API. It //! writes a single-line human-readable record to stderr for each server event. //! No state is required, so `user_data` is left null. //! //! # Building //! //! ```bash //! cargo build --release -p logging_plugin //! # Output: target/release/liblogging_plugin.so (Linux) //! # target/release/liblogging_plugin.dylib (macOS) //! ``` //! //! # Deploying //! //! ```bash //! cp target/release/liblogging_plugin.so /etc/qpc/plugins/ //! qpc-server --plugin-dir /etc/qpc/plugins //! ``` use std::ffi::c_void; use std::slice; use quicprochat_plugin_api::{ CAuthEvent, CChannelEvent, CFetchEvent, CMessageEvent, HookVTable, HOOK_CONTINUE, PLUGIN_OK, }; // ── Helpers ─────────────────────────────────────────────────────────────────── fn hex_prefix(ptr: *const u8, len: usize) -> String { if ptr.is_null() || len == 0 { return "(none)".to_string(); } let bytes = unsafe { slice::from_raw_parts(ptr, len.min(4)) }; bytes.iter().map(|b| format!("{:02x}", b)).collect() } fn str_from_raw(ptr: *const u8, len: usize) -> &'static str { if ptr.is_null() || len == 0 { return ""; } // Safety: the server owns the memory and it remains valid for the callback duration. let bytes = unsafe { slice::from_raw_parts(ptr, len) }; std::str::from_utf8(bytes).unwrap_or("") } // ── Hook callbacks ──────────────────────────────────────────────────────────── unsafe extern "C" fn on_message_enqueue( _user_data: *mut c_void, event: *const CMessageEvent, ) -> i32 { let e = &*event; eprintln!( "[qpc-plugin:logging] enqueue: recipient={} payload_len={} seq={} has_sender={}", hex_prefix(e.recipient_key, e.recipient_key_len), e.payload_len, e.seq, !e.sender_identity.is_null(), ); HOOK_CONTINUE } unsafe extern "C" fn on_batch_enqueue( _user_data: *mut c_void, events: *const CMessageEvent, count: usize, ) { eprintln!("[qpc-plugin:logging] batch_enqueue: count={}", count); let events = slice::from_raw_parts(events, count); for (i, e) in events.iter().enumerate() { eprintln!( "[qpc-plugin:logging] [{}/{}] recipient={} seq={}", i + 1, count, hex_prefix(e.recipient_key, e.recipient_key_len), e.seq, ); } } unsafe extern "C" fn on_auth(_user_data: *mut c_void, event: *const CAuthEvent) { let e = &*event; let username = str_from_raw(e.username, e.username_len); if e.success != 0 { eprintln!("[qpc-plugin:logging] auth: user='{}' SUCCESS", username); } else { let reason = str_from_raw(e.failure_reason, e.failure_reason_len); eprintln!( "[qpc-plugin:logging] auth: user='{}' FAILURE reason='{}'", username, reason ); } } unsafe extern "C" fn on_channel_created( _user_data: *mut c_void, event: *const CChannelEvent, ) { let e = &*event; eprintln!( "[qpc-plugin:logging] channel_created: channel={} was_new={} initiator={}", hex_prefix(e.channel_id, e.channel_id_len), e.was_new != 0, hex_prefix(e.initiator_key, e.initiator_key_len), ); } unsafe extern "C" fn on_fetch(_user_data: *mut c_void, event: *const CFetchEvent) { let e = &*event; if e.message_count > 0 { eprintln!( "[qpc-plugin:logging] fetch: recipient={} count={}", hex_prefix(e.recipient_key, e.recipient_key_len), e.message_count, ); } } unsafe extern "C" fn on_user_registered( _user_data: *mut c_void, username: *const u8, username_len: usize, identity_key: *const u8, identity_key_len: usize, ) { let name = str_from_raw(username, username_len); eprintln!( "[qpc-plugin:logging] user_registered: user='{}' key={}", name, hex_prefix(identity_key, identity_key_len), ); } // ── Plugin entry point ──────────────────────────────────────────────────────── /// Called by the server once at startup. /// /// # Safety /// /// `vtable` must point to a zeroed `HookVTable` as provided by `qpc-server`. #[no_mangle] pub unsafe extern "C" fn qpc_plugin_init(vtable: *mut HookVTable) -> i32 { if vtable.is_null() { return -1; } let v = &mut *vtable; // user_data is not needed — all callbacks are stateless. v.user_data = std::ptr::null_mut(); v.on_message_enqueue = Some(on_message_enqueue); v.on_batch_enqueue = Some(on_batch_enqueue); v.on_auth = Some(on_auth); v.on_channel_created = Some(on_channel_created); v.on_fetch = Some(on_fetch); v.on_user_registered = Some(on_user_registered); // error_message and destroy not needed (no state, never rejects). eprintln!("[qpc-plugin:logging] initialized"); PLUGIN_OK }