//! Custom wa-rs storage backend using ZeroClaw's rusqlite //! //! This module implements all 4 wa-rs storage traits using rusqlite directly, //! avoiding the Diesel/libsqlite3-sys dependency conflict from wa-rs-sqlite-storage. //! //! # Traits Implemented //! //! - [`SignalStore`]: Signal protocol cryptographic operations //! - [`AppSyncStore`]: WhatsApp app state synchronization //! - [`ProtocolStore`]: WhatsApp Web protocol alignment //! - [`DeviceStore`]: Device persistence operations #[cfg(feature = "whatsapp-web")] use async_trait::async_trait; #[cfg(feature = "whatsapp-web")] use parking_lot::Mutex; #[cfg(feature = "whatsapp-web")] use rusqlite::{params, Connection}; #[cfg(feature = "whatsapp-web")] use std::path::Path; #[cfg(feature = "whatsapp-web")] use std::sync::Arc; #[cfg(feature = "whatsapp-web")] use prost::Message; #[cfg(feature = "whatsapp-web")] use wa_rs_binary::jid::Jid; #[cfg(feature = "whatsapp-web")] use wa_rs_core::appstate::hash::HashState; #[cfg(feature = "whatsapp-web")] use wa_rs_core::appstate::processor::AppStateMutationMAC; #[cfg(feature = "whatsapp-web")] use wa_rs_core::store::traits::DeviceInfo; #[cfg(feature = "whatsapp-web")] use wa_rs_core::store::traits::DeviceStore as DeviceStoreTrait; #[cfg(feature = "whatsapp-web")] use wa_rs_core::store::traits::*; #[cfg(feature = "whatsapp-web")] use wa_rs_core::store::Device as CoreDevice; /// Custom wa-rs storage backend using rusqlite /// /// This implements all 4 storage traits required by wa-rs. /// The backend uses ZeroClaw's existing rusqlite setup, avoiding the /// Diesel/libsqlite3-sys conflict from wa-rs-sqlite-storage. #[cfg(feature = "whatsapp-web")] #[derive(Clone)] pub struct RusqliteStore { /// Database file path db_path: String, /// SQLite connection (thread-safe via Mutex) conn: Arc>, /// Device ID for this session device_id: i32, } /// Helper macro to convert rusqlite errors to StoreError /// For execute statements that return usize, maps to () macro_rules! to_store_err { // For expressions returning Result (execute: $expr:expr) => { $expr .map(|_| ()) .map_err(|e| wa_rs_core::store::error::StoreError::Database(e.to_string())) }; // For other expressions ($expr:expr) => { $expr.map_err(|e| wa_rs_core::store::error::StoreError::Database(e.to_string())) }; } #[cfg(feature = "whatsapp-web")] impl RusqliteStore { /// Create a new rusqlite-based storage backend /// /// # Arguments /// /// * `db_path` - Path to the SQLite database file (will be created if needed) pub fn new>(db_path: P) -> anyhow::Result { let db_path = db_path.as_ref().to_string_lossy().to_string(); // Create parent directory if needed if let Some(parent) = Path::new(&db_path).parent() { std::fs::create_dir_all(parent)?; } let conn = Connection::open(&db_path)?; // Enable WAL mode for better concurrency to_store_err!(conn.execute_batch( "PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL;", ))?; let store = Self { db_path, conn: Arc::new(Mutex::new(conn)), device_id: 1, // Default device ID }; store.init_schema()?; Ok(store) } /// Initialize all database tables fn init_schema(&self) -> anyhow::Result<()> { let conn = self.conn.lock(); to_store_err!(conn.execute_batch( "-- Main device table CREATE TABLE IF NOT EXISTS device ( id INTEGER PRIMARY KEY, lid TEXT, pn TEXT, registration_id INTEGER NOT NULL, noise_key BLOB NOT NULL, identity_key BLOB NOT NULL, signed_pre_key BLOB NOT NULL, signed_pre_key_id INTEGER NOT NULL, signed_pre_key_signature BLOB NOT NULL, adv_secret_key BLOB NOT NULL, account BLOB, push_name TEXT NOT NULL, app_version_primary INTEGER NOT NULL, app_version_secondary INTEGER NOT NULL, app_version_tertiary INTEGER NOT NULL, app_version_last_fetched_ms INTEGER NOT NULL, edge_routing_info BLOB, props_hash TEXT ); -- Signal identity keys CREATE TABLE IF NOT EXISTS identities ( address TEXT NOT NULL, key BLOB NOT NULL, device_id INTEGER NOT NULL, PRIMARY KEY (address, device_id) ); -- Signal protocol sessions CREATE TABLE IF NOT EXISTS sessions ( address TEXT NOT NULL, record BLOB NOT NULL, device_id INTEGER NOT NULL, PRIMARY KEY (address, device_id) ); -- Pre-keys for key exchange CREATE TABLE IF NOT EXISTS prekeys ( id INTEGER NOT NULL, key BLOB NOT NULL, uploaded INTEGER NOT NULL DEFAULT 0, device_id INTEGER NOT NULL, PRIMARY KEY (id, device_id) ); -- Signed pre-keys CREATE TABLE IF NOT EXISTS signed_prekeys ( id INTEGER NOT NULL, record BLOB NOT NULL, device_id INTEGER NOT NULL, PRIMARY KEY (id, device_id) ); -- Sender keys for group messaging CREATE TABLE IF NOT EXISTS sender_keys ( address TEXT NOT NULL, record BLOB NOT NULL, device_id INTEGER NOT NULL, PRIMARY KEY (address, device_id) ); -- App state sync keys CREATE TABLE IF NOT EXISTS app_state_keys ( key_id BLOB NOT NULL, key_data BLOB NOT NULL, device_id INTEGER NOT NULL, PRIMARY KEY (key_id, device_id) ); -- App state versions CREATE TABLE IF NOT EXISTS app_state_versions ( name TEXT NOT NULL, state_data BLOB NOT NULL, device_id INTEGER NOT NULL, PRIMARY KEY (name, device_id) ); -- App state mutation MACs CREATE TABLE IF NOT EXISTS app_state_mutation_macs ( name TEXT NOT NULL, version INTEGER NOT NULL, index_mac BLOB NOT NULL, value_mac BLOB NOT NULL, device_id INTEGER NOT NULL, PRIMARY KEY (name, index_mac, device_id) ); -- LID to phone number mapping CREATE TABLE IF NOT EXISTS lid_pn_mapping ( lid TEXT NOT NULL, phone_number TEXT NOT NULL, created_at INTEGER NOT NULL, learning_source TEXT NOT NULL, updated_at INTEGER NOT NULL, device_id INTEGER NOT NULL, PRIMARY KEY (lid, device_id) ); -- SKDM recipients tracking CREATE TABLE IF NOT EXISTS skdm_recipients ( group_jid TEXT NOT NULL, device_jid TEXT NOT NULL, device_id INTEGER NOT NULL, created_at INTEGER NOT NULL, PRIMARY KEY (group_jid, device_jid, device_id) ); -- Device registry for multi-device CREATE TABLE IF NOT EXISTS device_registry ( user_id TEXT NOT NULL, devices_json TEXT NOT NULL, timestamp INTEGER NOT NULL, phash TEXT, device_id INTEGER NOT NULL, updated_at INTEGER NOT NULL, PRIMARY KEY (user_id, device_id) ); -- Base keys for collision detection CREATE TABLE IF NOT EXISTS base_keys ( address TEXT NOT NULL, message_id TEXT NOT NULL, base_key BLOB NOT NULL, device_id INTEGER NOT NULL, created_at INTEGER NOT NULL, PRIMARY KEY (address, message_id, device_id) ); -- Sender key status for lazy deletion CREATE TABLE IF NOT EXISTS sender_key_status ( group_jid TEXT NOT NULL, participant TEXT NOT NULL, device_id INTEGER NOT NULL, marked_at INTEGER NOT NULL, PRIMARY KEY (group_jid, participant, device_id) ); -- Trusted contact tokens CREATE TABLE IF NOT EXISTS tc_tokens ( jid TEXT NOT NULL, token BLOB NOT NULL, token_timestamp INTEGER NOT NULL, sender_timestamp INTEGER, device_id INTEGER NOT NULL, updated_at INTEGER NOT NULL, PRIMARY KEY (jid, device_id) );", ))?; Ok(()) } } #[cfg(feature = "whatsapp-web")] #[async_trait] impl SignalStore for RusqliteStore { // --- Identity Operations --- async fn put_identity( &self, address: &str, key: [u8; 32], ) -> wa_rs_core::store::error::Result<()> { let conn = self.conn.lock(); to_store_err!(execute: conn.execute( "INSERT OR REPLACE INTO identities (address, key, device_id) VALUES (?1, ?2, ?3)", params![address, key.to_vec(), self.device_id], )) } async fn load_identity( &self, address: &str, ) -> wa_rs_core::store::error::Result>> { let conn = self.conn.lock(); let result = conn.query_row( "SELECT key FROM identities WHERE address = ?1 AND device_id = ?2", params![address, self.device_id], |row| row.get::<_, Vec>(0), ); match result { Ok(key) => Ok(Some(key)), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), Err(e) => Err(wa_rs_core::store::error::StoreError::Database( e.to_string(), )), } } async fn delete_identity(&self, address: &str) -> wa_rs_core::store::error::Result<()> { let conn = self.conn.lock(); to_store_err!(execute: conn.execute( "DELETE FROM identities WHERE address = ?1 AND device_id = ?2", params![address, self.device_id], )) } // --- Session Operations --- async fn get_session( &self, address: &str, ) -> wa_rs_core::store::error::Result>> { let conn = self.conn.lock(); let result = conn.query_row( "SELECT record FROM sessions WHERE address = ?1 AND device_id = ?2", params![address, self.device_id], |row| row.get::<_, Vec>(0), ); match result { Ok(record) => Ok(Some(record)), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), Err(e) => Err(wa_rs_core::store::error::StoreError::Database( e.to_string(), )), } } async fn put_session( &self, address: &str, session: &[u8], ) -> wa_rs_core::store::error::Result<()> { let conn = self.conn.lock(); to_store_err!(execute: conn.execute( "INSERT OR REPLACE INTO sessions (address, record, device_id) VALUES (?1, ?2, ?3)", params![address, session, self.device_id], )) } async fn delete_session(&self, address: &str) -> wa_rs_core::store::error::Result<()> { let conn = self.conn.lock(); to_store_err!(execute: conn.execute( "DELETE FROM sessions WHERE address = ?1 AND device_id = ?2", params![address, self.device_id], )) } // --- PreKey Operations --- async fn store_prekey( &self, id: u32, record: &[u8], uploaded: bool, ) -> wa_rs_core::store::error::Result<()> { let conn = self.conn.lock(); to_store_err!(execute: conn.execute( "INSERT OR REPLACE INTO prekeys (id, key, uploaded, device_id) VALUES (?1, ?2, ?3, ?4)", params![id, record, uploaded, self.device_id], )) } async fn load_prekey(&self, id: u32) -> wa_rs_core::store::error::Result>> { let conn = self.conn.lock(); let result = conn.query_row( "SELECT key FROM prekeys WHERE id = ?1 AND device_id = ?2", params![id, self.device_id], |row| row.get::<_, Vec>(0), ); match result { Ok(key) => Ok(Some(key)), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), Err(e) => Err(wa_rs_core::store::error::StoreError::Database( e.to_string(), )), } } async fn remove_prekey(&self, id: u32) -> wa_rs_core::store::error::Result<()> { let conn = self.conn.lock(); to_store_err!(execute: conn.execute( "DELETE FROM prekeys WHERE id = ?1 AND device_id = ?2", params![id, self.device_id], )) } // --- Signed PreKey Operations --- async fn store_signed_prekey( &self, id: u32, record: &[u8], ) -> wa_rs_core::store::error::Result<()> { let conn = self.conn.lock(); to_store_err!(execute: conn.execute( "INSERT OR REPLACE INTO signed_prekeys (id, record, device_id) VALUES (?1, ?2, ?3)", params![id, record, self.device_id], )) } async fn load_signed_prekey( &self, id: u32, ) -> wa_rs_core::store::error::Result>> { let conn = self.conn.lock(); let result = conn.query_row( "SELECT record FROM signed_prekeys WHERE id = ?1 AND device_id = ?2", params![id, self.device_id], |row| row.get::<_, Vec>(0), ); match result { Ok(record) => Ok(Some(record)), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), Err(e) => Err(wa_rs_core::store::error::StoreError::Database( e.to_string(), )), } } async fn load_all_signed_prekeys( &self, ) -> wa_rs_core::store::error::Result)>> { let conn = self.conn.lock(); let mut stmt = to_store_err!( conn.prepare("SELECT id, record FROM signed_prekeys WHERE device_id = ?1") )?; let rows = to_store_err!(stmt.query_map(params![self.device_id], |row| { Ok((row.get::<_, u32>(0)?, row.get::<_, Vec>(1)?)) }))?; let mut result = Vec::new(); for row in rows { result.push(to_store_err!(row)?); } Ok(result) } async fn remove_signed_prekey(&self, id: u32) -> wa_rs_core::store::error::Result<()> { let conn = self.conn.lock(); to_store_err!(execute: conn.execute( "DELETE FROM signed_prekeys WHERE id = ?1 AND device_id = ?2", params![id, self.device_id], )) } // --- Sender Key Operations --- async fn put_sender_key( &self, address: &str, record: &[u8], ) -> wa_rs_core::store::error::Result<()> { let conn = self.conn.lock(); to_store_err!(execute: conn.execute( "INSERT OR REPLACE INTO sender_keys (address, record, device_id) VALUES (?1, ?2, ?3)", params![address, record, self.device_id], )) } async fn get_sender_key( &self, address: &str, ) -> wa_rs_core::store::error::Result>> { let conn = self.conn.lock(); let result = conn.query_row( "SELECT record FROM sender_keys WHERE address = ?1 AND device_id = ?2", params![address, self.device_id], |row| row.get::<_, Vec>(0), ); match result { Ok(record) => Ok(Some(record)), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), Err(e) => Err(wa_rs_core::store::error::StoreError::Database( e.to_string(), )), } } async fn delete_sender_key(&self, address: &str) -> wa_rs_core::store::error::Result<()> { let conn = self.conn.lock(); to_store_err!(execute: conn.execute( "DELETE FROM sender_keys WHERE address = ?1 AND device_id = ?2", params![address, self.device_id], )) } } #[cfg(feature = "whatsapp-web")] #[async_trait] impl AppSyncStore for RusqliteStore { async fn get_sync_key( &self, key_id: &[u8], ) -> wa_rs_core::store::error::Result> { let conn = self.conn.lock(); let result = conn.query_row( "SELECT key_data FROM app_state_keys WHERE key_id = ?1 AND device_id = ?2", params![key_id, self.device_id], |row| { let key_data: Vec = row.get(0)?; serde_json::from_slice(&key_data) .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e))) }, ); match result { Ok(key) => Ok(Some(key)), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), Err(e) => Err(wa_rs_core::store::error::StoreError::Database( e.to_string(), )), } } async fn set_sync_key( &self, key_id: &[u8], key: AppStateSyncKey, ) -> wa_rs_core::store::error::Result<()> { let conn = self.conn.lock(); let key_data = to_store_err!(serde_json::to_vec(&key))?; to_store_err!(execute: conn.execute( "INSERT OR REPLACE INTO app_state_keys (key_id, key_data, device_id) VALUES (?1, ?2, ?3)", params![key_id, key_data, self.device_id], )) } async fn get_version(&self, name: &str) -> wa_rs_core::store::error::Result { let conn = self.conn.lock(); let state_data: Vec = to_store_err!(conn.query_row( "SELECT state_data FROM app_state_versions WHERE name = ?1 AND device_id = ?2", params![name, self.device_id], |row| row.get(0), ))?; to_store_err!(serde_json::from_slice(&state_data)) } async fn set_version( &self, name: &str, state: HashState, ) -> wa_rs_core::store::error::Result<()> { let conn = self.conn.lock(); let state_data = to_store_err!(serde_json::to_vec(&state))?; to_store_err!(execute: conn.execute( "INSERT OR REPLACE INTO app_state_versions (name, state_data, device_id) VALUES (?1, ?2, ?3)", params![name, state_data, self.device_id], )) } async fn put_mutation_macs( &self, name: &str, version: u64, mutations: &[AppStateMutationMAC], ) -> wa_rs_core::store::error::Result<()> { let conn = self.conn.lock(); for mutation in mutations { let index_mac = to_store_err!(serde_json::to_vec(&mutation.index_mac))?; let value_mac = to_store_err!(serde_json::to_vec(&mutation.value_mac))?; to_store_err!(execute: conn.execute( "INSERT OR REPLACE INTO app_state_mutation_macs (name, version, index_mac, value_mac, device_id) VALUES (?1, ?2, ?3, ?4, ?5)", params![name, i64::try_from(version).unwrap_or(i64::MAX), index_mac, value_mac, self.device_id], ))?; } Ok(()) } async fn get_mutation_mac( &self, name: &str, index_mac: &[u8], ) -> wa_rs_core::store::error::Result>> { let conn = self.conn.lock(); let index_mac_json = to_store_err!(serde_json::to_vec(index_mac))?; let result = conn.query_row( "SELECT value_mac FROM app_state_mutation_macs WHERE name = ?1 AND index_mac = ?2 AND device_id = ?3", params![name, index_mac_json, self.device_id], |row| row.get::<_, Vec>(0), ); match result { Ok(mac) => Ok(Some(mac)), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), Err(e) => Err(wa_rs_core::store::error::StoreError::Database( e.to_string(), )), } } async fn delete_mutation_macs( &self, name: &str, index_macs: &[Vec], ) -> wa_rs_core::store::error::Result<()> { let conn = self.conn.lock(); for index_mac in index_macs { let index_mac_json = to_store_err!(serde_json::to_vec(index_mac))?; to_store_err!(execute: conn.execute( "DELETE FROM app_state_mutation_macs WHERE name = ?1 AND index_mac = ?2 AND device_id = ?3", params![name, index_mac_json, self.device_id], ))?; } Ok(()) } } #[cfg(feature = "whatsapp-web")] #[async_trait] impl ProtocolStore for RusqliteStore { // --- SKDM Tracking --- async fn get_skdm_recipients( &self, group_jid: &str, ) -> wa_rs_core::store::error::Result> { let conn = self.conn.lock(); let mut stmt = to_store_err!(conn.prepare( "SELECT device_jid FROM skdm_recipients WHERE group_jid = ?1 AND device_id = ?2" ))?; let rows = to_store_err!(stmt.query_map(params![group_jid, self.device_id], |row| { row.get::<_, String>(0) }))?; let mut result = Vec::new(); for row in rows { let jid_str = to_store_err!(row)?; if let Ok(jid) = jid_str.parse() { result.push(jid); } } Ok(result) } async fn add_skdm_recipients( &self, group_jid: &str, device_jids: &[Jid], ) -> wa_rs_core::store::error::Result<()> { let conn = self.conn.lock(); let now = chrono::Utc::now().timestamp(); for device_jid in device_jids { to_store_err!(execute: conn.execute( "INSERT OR IGNORE INTO skdm_recipients (group_jid, device_jid, device_id, created_at) VALUES (?1, ?2, ?3, ?4)", params![group_jid, device_jid.to_string(), self.device_id, now], ))?; } Ok(()) } async fn clear_skdm_recipients(&self, group_jid: &str) -> wa_rs_core::store::error::Result<()> { let conn = self.conn.lock(); to_store_err!(execute: conn.execute( "DELETE FROM skdm_recipients WHERE group_jid = ?1 AND device_id = ?2", params![group_jid, self.device_id], )) } // --- LID-PN Mapping --- async fn get_lid_mapping( &self, lid: &str, ) -> wa_rs_core::store::error::Result> { let conn = self.conn.lock(); let result = conn.query_row( "SELECT lid, phone_number, created_at, learning_source, updated_at FROM lid_pn_mapping WHERE lid = ?1 AND device_id = ?2", params![lid, self.device_id], |row| { Ok(LidPnMappingEntry { lid: row.get(0)?, phone_number: row.get(1)?, created_at: row.get(2)?, learning_source: row.get(3)?, updated_at: row.get(4)?, }) }, ); match result { Ok(entry) => Ok(Some(entry)), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), Err(e) => Err(wa_rs_core::store::error::StoreError::Database( e.to_string(), )), } } async fn get_pn_mapping( &self, phone: &str, ) -> wa_rs_core::store::error::Result> { let conn = self.conn.lock(); let result = conn.query_row( "SELECT lid, phone_number, created_at, learning_source, updated_at FROM lid_pn_mapping WHERE phone_number = ?1 AND device_id = ?2 ORDER BY updated_at DESC LIMIT 1", params![phone, self.device_id], |row| { Ok(LidPnMappingEntry { lid: row.get(0)?, phone_number: row.get(1)?, created_at: row.get(2)?, learning_source: row.get(3)?, updated_at: row.get(4)?, }) }, ); match result { Ok(entry) => Ok(Some(entry)), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), Err(e) => Err(wa_rs_core::store::error::StoreError::Database( e.to_string(), )), } } async fn put_lid_mapping( &self, entry: &LidPnMappingEntry, ) -> wa_rs_core::store::error::Result<()> { let conn = self.conn.lock(); to_store_err!(execute: conn.execute( "INSERT OR REPLACE INTO lid_pn_mapping (lid, phone_number, created_at, learning_source, updated_at, device_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", params![ entry.lid, entry.phone_number, entry.created_at, entry.learning_source, entry.updated_at, self.device_id, ], )) } async fn get_all_lid_mappings( &self, ) -> wa_rs_core::store::error::Result> { let conn = self.conn.lock(); let mut stmt = to_store_err!(conn.prepare( "SELECT lid, phone_number, created_at, learning_source, updated_at FROM lid_pn_mapping WHERE device_id = ?1" ))?; let rows = to_store_err!(stmt.query_map(params![self.device_id], |row| { Ok(LidPnMappingEntry { lid: row.get(0)?, phone_number: row.get(1)?, created_at: row.get(2)?, learning_source: row.get(3)?, updated_at: row.get(4)?, }) }))?; let mut result = Vec::new(); for row in rows { result.push(to_store_err!(row)?); } Ok(result) } // --- Base Key Collision Detection --- async fn save_base_key( &self, address: &str, message_id: &str, base_key: &[u8], ) -> wa_rs_core::store::error::Result<()> { let conn = self.conn.lock(); let now = chrono::Utc::now().timestamp(); to_store_err!(execute: conn.execute( "INSERT OR REPLACE INTO base_keys (address, message_id, base_key, device_id, created_at) VALUES (?1, ?2, ?3, ?4, ?5)", params![address, message_id, base_key, self.device_id, now], )) } async fn has_same_base_key( &self, address: &str, message_id: &str, current_base_key: &[u8], ) -> wa_rs_core::store::error::Result { let conn = self.conn.lock(); let result = conn.query_row( "SELECT base_key FROM base_keys WHERE address = ?1 AND message_id = ?2 AND device_id = ?3", params![address, message_id, self.device_id], |row| { let saved_key: Vec = row.get(0)?; Ok(saved_key == current_base_key) }, ); match result { Ok(same) => Ok(same), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(false), Err(e) => Err(wa_rs_core::store::error::StoreError::Database( e.to_string(), )), } } async fn delete_base_key( &self, address: &str, message_id: &str, ) -> wa_rs_core::store::error::Result<()> { let conn = self.conn.lock(); to_store_err!(execute: conn.execute( "DELETE FROM base_keys WHERE address = ?1 AND message_id = ?2 AND device_id = ?3", params![address, message_id, self.device_id], )) } // --- Device Registry --- async fn update_device_list( &self, record: DeviceListRecord, ) -> wa_rs_core::store::error::Result<()> { let conn = self.conn.lock(); let devices_json = to_store_err!(serde_json::to_string(&record.devices))?; let now = chrono::Utc::now().timestamp(); to_store_err!(execute: conn.execute( "INSERT OR REPLACE INTO device_registry (user_id, devices_json, timestamp, phash, device_id, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", params![ record.user, devices_json, record.timestamp, record.phash, self.device_id, now, ], )) } async fn get_devices( &self, user: &str, ) -> wa_rs_core::store::error::Result> { let conn = self.conn.lock(); let result = conn.query_row( "SELECT user_id, devices_json, timestamp, phash FROM device_registry WHERE user_id = ?1 AND device_id = ?2", params![user, self.device_id], |row| { // Helper to convert errors to rusqlite::Error fn to_rusqlite_err( e: E, ) -> rusqlite::Error { rusqlite::Error::ToSqlConversionFailure(Box::new(e)) } let devices_json: String = row.get(1)?; let devices: Vec = serde_json::from_str(&devices_json).map_err(to_rusqlite_err)?; Ok(DeviceListRecord { user: row.get(0)?, devices, timestamp: row.get(2)?, phash: row.get(3)?, }) }, ); match result { Ok(record) => Ok(Some(record)), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), Err(e) => Err(wa_rs_core::store::error::StoreError::Database( e.to_string(), )), } } // --- Sender Key Status (Lazy Deletion) --- async fn mark_forget_sender_key( &self, group_jid: &str, participant: &str, ) -> wa_rs_core::store::error::Result<()> { let conn = self.conn.lock(); let now = chrono::Utc::now().timestamp(); to_store_err!(execute: conn.execute( "INSERT OR REPLACE INTO sender_key_status (group_jid, participant, device_id, marked_at) VALUES (?1, ?2, ?3, ?4)", params![group_jid, participant, self.device_id, now], )) } async fn consume_forget_marks( &self, group_jid: &str, ) -> wa_rs_core::store::error::Result> { let conn = self.conn.lock(); let mut stmt = to_store_err!(conn.prepare( "SELECT participant FROM sender_key_status WHERE group_jid = ?1 AND device_id = ?2" ))?; let rows = to_store_err!(stmt.query_map(params![group_jid, self.device_id], |row| { row.get::<_, String>(0) }))?; let mut result = Vec::new(); for row in rows { result.push(to_store_err!(row)?); } // Delete the marks after consuming them to_store_err!(execute: conn.execute( "DELETE FROM sender_key_status WHERE group_jid = ?1 AND device_id = ?2", params![group_jid, self.device_id], ))?; Ok(result) } // --- TcToken Storage --- async fn get_tc_token( &self, jid: &str, ) -> wa_rs_core::store::error::Result> { let conn = self.conn.lock(); let result = conn.query_row( "SELECT token, token_timestamp, sender_timestamp FROM tc_tokens WHERE jid = ?1 AND device_id = ?2", params![jid, self.device_id], |row| { Ok(TcTokenEntry { token: row.get(0)?, token_timestamp: row.get(1)?, sender_timestamp: row.get(2)?, }) }, ); match result { Ok(entry) => Ok(Some(entry)), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), Err(e) => Err(wa_rs_core::store::error::StoreError::Database( e.to_string(), )), } } async fn put_tc_token( &self, jid: &str, entry: &TcTokenEntry, ) -> wa_rs_core::store::error::Result<()> { let conn = self.conn.lock(); let now = chrono::Utc::now().timestamp(); to_store_err!(execute: conn.execute( "INSERT OR REPLACE INTO tc_tokens (jid, token, token_timestamp, sender_timestamp, device_id, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", params![ jid, entry.token, entry.token_timestamp, entry.sender_timestamp, self.device_id, now, ], )) } async fn delete_tc_token(&self, jid: &str) -> wa_rs_core::store::error::Result<()> { let conn = self.conn.lock(); to_store_err!(execute: conn.execute( "DELETE FROM tc_tokens WHERE jid = ?1 AND device_id = ?2", params![jid, self.device_id], )) } async fn get_all_tc_token_jids(&self) -> wa_rs_core::store::error::Result> { let conn = self.conn.lock(); let mut stmt = to_store_err!(conn.prepare("SELECT jid FROM tc_tokens WHERE device_id = ?1"))?; let rows = to_store_err!( stmt.query_map(params![self.device_id], |row| { row.get::<_, String>(0) }) )?; let mut result = Vec::new(); for row in rows { result.push(to_store_err!(row)?); } Ok(result) } async fn delete_expired_tc_tokens( &self, cutoff_timestamp: i64, ) -> wa_rs_core::store::error::Result { let conn = self.conn.lock(); let deleted = conn .execute( "DELETE FROM tc_tokens WHERE token_timestamp < ?1 AND device_id = ?2", params![cutoff_timestamp, self.device_id], ) .map_err(|e| wa_rs_core::store::error::StoreError::Database(e.to_string()))?; let deleted = u32::try_from(deleted).map_err(|_| { wa_rs_core::store::error::StoreError::Database(format!( "Affected row count overflowed u32: {deleted}" )) })?; Ok(deleted) } } #[cfg(feature = "whatsapp-web")] #[async_trait] impl DeviceStoreTrait for RusqliteStore { async fn save(&self, device: &CoreDevice) -> wa_rs_core::store::error::Result<()> { let conn = self.conn.lock(); // Serialize KeyPairs to bytes let noise_key = { let mut bytes = Vec::new(); let priv_key = device.noise_key.private_key.serialize(); bytes.extend_from_slice(priv_key.as_slice()); bytes.extend_from_slice(device.noise_key.public_key.public_key_bytes()); bytes }; let identity_key = { let mut bytes = Vec::new(); let priv_key = device.identity_key.private_key.serialize(); bytes.extend_from_slice(priv_key.as_slice()); bytes.extend_from_slice(device.identity_key.public_key.public_key_bytes()); bytes }; let signed_pre_key = { let mut bytes = Vec::new(); let priv_key = device.signed_pre_key.private_key.serialize(); bytes.extend_from_slice(priv_key.as_slice()); bytes.extend_from_slice(device.signed_pre_key.public_key.public_key_bytes()); bytes }; let account = device.account.as_ref().map(|a| a.encode_to_vec()); to_store_err!(execute: conn.execute( "INSERT OR REPLACE INTO device ( id, lid, pn, registration_id, noise_key, identity_key, signed_pre_key, signed_pre_key_id, signed_pre_key_signature, adv_secret_key, account, push_name, app_version_primary, app_version_secondary, app_version_tertiary, app_version_last_fetched_ms, edge_routing_info, props_hash ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18)", params![ self.device_id, device.lid.as_ref().map(|j| j.to_string()), device.pn.as_ref().map(|j| j.to_string()), device.registration_id, noise_key, identity_key, signed_pre_key, device.signed_pre_key_id, device.signed_pre_key_signature.to_vec(), device.adv_secret_key.to_vec(), account, &device.push_name, device.app_version_primary, device.app_version_secondary, device.app_version_tertiary, device.app_version_last_fetched_ms, device.edge_routing_info.as_ref().map(|v| v.clone()), device.props_hash.as_ref().map(|v| v.clone()), ], )) } async fn load(&self) -> wa_rs_core::store::error::Result> { let conn = self.conn.lock(); let result = conn.query_row( "SELECT * FROM device WHERE id = ?1", params![self.device_id], |row| { // Helper to convert errors to rusqlite::Error fn to_rusqlite_err( e: E, ) -> rusqlite::Error { rusqlite::Error::ToSqlConversionFailure(Box::new(e)) } // Deserialize KeyPairs from bytes (64 bytes each) let noise_key_bytes: Vec = row.get("noise_key")?; let identity_key_bytes: Vec = row.get("identity_key")?; let signed_pre_key_bytes: Vec = row.get("signed_pre_key")?; if noise_key_bytes.len() != 64 || identity_key_bytes.len() != 64 || signed_pre_key_bytes.len() != 64 { return Err(rusqlite::Error::InvalidParameterName("key_pair".into())); } use wa_rs_core::libsignal::protocol::{KeyPair, PrivateKey, PublicKey}; let noise_key = KeyPair::new( PublicKey::from_djb_public_key_bytes(&noise_key_bytes[32..64]) .map_err(to_rusqlite_err)?, PrivateKey::deserialize(&noise_key_bytes[0..32]).map_err(to_rusqlite_err)?, ); let identity_key = KeyPair::new( PublicKey::from_djb_public_key_bytes(&identity_key_bytes[32..64]) .map_err(to_rusqlite_err)?, PrivateKey::deserialize(&identity_key_bytes[0..32]).map_err(to_rusqlite_err)?, ); let signed_pre_key = KeyPair::new( PublicKey::from_djb_public_key_bytes(&signed_pre_key_bytes[32..64]) .map_err(to_rusqlite_err)?, PrivateKey::deserialize(&signed_pre_key_bytes[0..32]) .map_err(to_rusqlite_err)?, ); let lid_str: Option = row.get("lid")?; let pn_str: Option = row.get("pn")?; let signature_bytes: Vec = row.get("signed_pre_key_signature")?; let adv_secret_bytes: Vec = row.get("adv_secret_key")?; let account_bytes: Option> = row.get("account")?; let mut signature = [0u8; 64]; let mut adv_secret = [0u8; 32]; signature.copy_from_slice(&signature_bytes); adv_secret.copy_from_slice(&adv_secret_bytes); let account = if let Some(bytes) = account_bytes { Some( wa_rs_proto::whatsapp::AdvSignedDeviceIdentity::decode(&*bytes) .map_err(to_rusqlite_err)?, ) } else { None }; Ok(CoreDevice { lid: lid_str.and_then(|s| s.parse().ok()), pn: pn_str.and_then(|s| s.parse().ok()), registration_id: row.get("registration_id")?, noise_key, identity_key, signed_pre_key, signed_pre_key_id: row.get("signed_pre_key_id")?, signed_pre_key_signature: signature, adv_secret_key: adv_secret, account, push_name: row.get("push_name")?, app_version_primary: row.get("app_version_primary")?, app_version_secondary: row.get("app_version_secondary")?, app_version_tertiary: row.get("app_version_tertiary")?, app_version_last_fetched_ms: row.get("app_version_last_fetched_ms")?, edge_routing_info: row.get("edge_routing_info")?, props_hash: row.get("props_hash")?, ..Default::default() }) }, ); match result { Ok(device) => Ok(Some(device)), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), Err(e) => Err(wa_rs_core::store::error::StoreError::Database( e.to_string(), )), } } async fn exists(&self) -> wa_rs_core::store::error::Result { let conn = self.conn.lock(); let count: i64 = to_store_err!(conn.query_row( "SELECT COUNT(*) FROM device WHERE id = ?1", params![self.device_id], |row| row.get(0), ))?; Ok(count > 0) } async fn create(&self) -> wa_rs_core::store::error::Result { // Device already created in constructor, just return the ID Ok(self.device_id) } async fn snapshot_db( &self, name: &str, extra_content: Option<&[u8]>, ) -> wa_rs_core::store::error::Result<()> { // Create a snapshot by copying the database file let snapshot_path = format!("{}.snapshot.{}", self.db_path, name); to_store_err!(std::fs::copy(&self.db_path, &snapshot_path))?; // If extra_content is provided, save it alongside if let Some(content) = extra_content { let content_path = format!("{}.extra", snapshot_path); to_store_err!(std::fs::write(&content_path, content))?; } Ok(()) } } #[cfg(test)] mod tests { use super::*; #[cfg(feature = "whatsapp-web")] use wa_rs_core::store::traits::{LidPnMappingEntry, ProtocolStore, TcTokenEntry}; #[cfg(feature = "whatsapp-web")] #[test] fn rusqlite_store_creates_database() { let tmp = tempfile::NamedTempFile::new().unwrap(); let store = RusqliteStore::new(tmp.path()).unwrap(); assert_eq!(store.device_id, 1); } #[cfg(feature = "whatsapp-web")] #[tokio::test] async fn lid_mapping_round_trip_preserves_learning_source_and_updated_at() { let tmp = tempfile::NamedTempFile::new().unwrap(); let store = RusqliteStore::new(tmp.path()).unwrap(); let entry = LidPnMappingEntry { lid: "100000012345678".to_string(), phone_number: "15551234567".to_string(), created_at: 1_700_000_000, updated_at: 1_700_000_100, learning_source: "usync".to_string(), }; ProtocolStore::put_lid_mapping(&store, &entry) .await .unwrap(); let loaded = ProtocolStore::get_lid_mapping(&store, &entry.lid) .await .unwrap() .expect("expected lid mapping to be present"); assert_eq!(loaded.learning_source, entry.learning_source); assert_eq!(loaded.updated_at, entry.updated_at); let loaded_by_pn = ProtocolStore::get_pn_mapping(&store, &entry.phone_number) .await .unwrap() .expect("expected pn mapping to be present"); assert_eq!(loaded_by_pn.learning_source, entry.learning_source); assert_eq!(loaded_by_pn.updated_at, entry.updated_at); } #[cfg(feature = "whatsapp-web")] #[tokio::test] async fn delete_expired_tc_tokens_returns_deleted_row_count() { let tmp = tempfile::NamedTempFile::new().unwrap(); let store = RusqliteStore::new(tmp.path()).unwrap(); let expired = TcTokenEntry { token: vec![1, 2, 3], token_timestamp: 10, sender_timestamp: None, }; let fresh = TcTokenEntry { token: vec![4, 5, 6], token_timestamp: 1000, sender_timestamp: Some(1000), }; ProtocolStore::put_tc_token(&store, "15550000001", &expired) .await .unwrap(); ProtocolStore::put_tc_token(&store, "15550000002", &fresh) .await .unwrap(); let deleted = ProtocolStore::delete_expired_tc_tokens(&store, 100) .await .unwrap(); assert_eq!(deleted, 1); assert!(ProtocolStore::get_tc_token(&store, "15550000001") .await .unwrap() .is_none()); assert!(ProtocolStore::get_tc_token(&store, "15550000002") .await .unwrap() .is_some()); } }