From c14864c8b7140342c7c7a744e2876e597f55d794 Mon Sep 17 00:00:00 2001 From: Alex Emmet <111742636+Alex-Emmet@users.noreply.github.com> Date: Mon, 16 Mar 2026 23:38:37 +0100 Subject: [PATCH] [Add] Basic SQLite Implementation --- Cargo.lock | 109 ++++++++-- Cargo.toml | 1 + src/main.rs | 2 +- src/omikron/omikron_connection.rs | 9 +- src/util/chat_files.rs | 340 ++++++++++++++++++------------ src/util/chats_util.rs | 158 ++++++++++---- 6 files changed, 417 insertions(+), 202 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6773323..eb69592 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -552,9 +552,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.56" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "jobserver", @@ -982,7 +982,7 @@ dependencies = [ [[package]] name = "epsilon-core" version = "0.1.0" -source = "git+https://github.com/Tensamin/Epsilon.git#809a32581843e663d35d4b3222f2cc59371d2786" +source = "git+https://github.com/Tensamin/Epsilon.git#d5edc693a91954834a38c39517b79092983eac5c" dependencies = [ "base64", "byteorder", @@ -994,7 +994,7 @@ dependencies = [ [[package]] name = "epsilon-native" version = "0.1.0" -source = "git+https://github.com/Tensamin/Epsilon.git#809a32581843e663d35d4b3222f2cc59371d2786" +source = "git+https://github.com/Tensamin/Epsilon.git#d5edc693a91954834a38c39517b79092983eac5c" dependencies = [ "epsilon-core", "quinn", @@ -1030,6 +1030,18 @@ dependencies = [ "num-traits", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fancy-regex" version = "0.11.0" @@ -1358,6 +1370,15 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashlink" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" +dependencies = [ + "hashbrown 0.16.1", +] + [[package]] name = "headers" version = "0.4.1" @@ -1714,9 +1735,9 @@ dependencies = [ [[package]] name = "instability" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d" +checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" dependencies = [ "darling", "indoc", @@ -1753,6 +1774,7 @@ dependencies = [ "rand_core 0.6.4", "ratatui", "reqwest", + "rusqlite", "rustls", "rustls-pemfile", "serde_json", @@ -1879,9 +1901,9 @@ checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" [[package]] name = "kasuari" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fe90c1150662e858c7d5f945089b7517b0a80d8bf7ba4b1b5ffc984e7230a5b" +checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" dependencies = [ "hashbrown 0.16.1", "portable-atomic", @@ -1924,6 +1946,16 @@ version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +[[package]] +name = "libsqlite3-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" +dependencies = [ + "pkg-config", + "vcpkg", +] + [[package]] name = "line-clipping" version = "0.3.5" @@ -2228,9 +2260,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "opaque-debug" @@ -2251,9 +2283,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.75" +version = "0.10.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" dependencies = [ "bitflags 2.11.0", "cfg-if", @@ -2283,9 +2315,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.111" +version = "0.9.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" dependencies = [ "cc", "libc", @@ -2984,6 +3016,31 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rsqlite-vfs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" +dependencies = [ + "hashbrown 0.16.1", + "thiserror 2.0.18", +] + +[[package]] +name = "rusqlite" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e" +dependencies = [ + "bitflags 2.11.0", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", + "sqlite-wasm-rs", +] + [[package]] name = "rustc-hash" version = "2.1.1" @@ -3130,9 +3187,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -3336,6 +3393,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4206ed3a67690b9c29b77d728f6acc3ce78f16bf846d83c94f76400320181b" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -3478,9 +3547,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.26.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom 0.4.2", @@ -3637,9 +3706,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] diff --git a/Cargo.toml b/Cargo.toml index 8315b65..b2fbe6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,3 +52,4 @@ ratatui = "0.30.0" open = "5.3.3" chrono = "0.4.43" serde_json = "1.0.149" +rusqlite = "0.39.0" diff --git a/src/main.rs b/src/main.rs index aa355bb..ae02090 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,7 +20,7 @@ use crate::gui::app_state::AppState; use crate::gui::screens::main_screen::MainScreen; use crate::gui::ui::start_tui; use crate::langu::language_creator; -use crate::omikron::omikron_connection::{OMIKRON_CONNECTION, OmikronConnection}; +use crate::omikron::omikron_connection::OmikronConnection; use crate::terms::consent_state; use crate::users::user_manager; use crate::util::config_util::CONFIG; diff --git a/src/omikron/omikron_connection.rs b/src/omikron/omikron_connection.rs index d1e22fa..ab2d1ca 100755 --- a/src/omikron/omikron_connection.rs +++ b/src/omikron/omikron_connection.rs @@ -604,12 +604,11 @@ impl OmikronConnection { let partner_id = cv.get_data(DataTypes::user_id).as_number().unwrap_or(0); let offset = cv.get_data(DataTypes::offset).as_number().unwrap_or(0); let amount = cv.get_data(DataTypes::amount).as_number().unwrap_or(0); - // let messages = chat_files::get_messages(my_id as i64, partner_id, offset, amount); + let messages = chat_files::get_messages(my_id as i64, partner_id, offset, amount); let resp = CommunicationValue::new(CommunicationType::messages_get) .with_id(cv.get_id()) .with_receiver(my_id) - //.add_data(DataTypes::messages, messages) - ; + .add_data(DataTypes::messages, DataValue::Str(messages.dump())); self.send_message(&resp).await; return; @@ -617,11 +616,11 @@ impl OmikronConnection { if cv.is_type(CommunicationType::get_chats) { let user_id = cv.get_sender(); - let users = chats_util::get_users(user_id as i64).as_i64().unwrap(); + let users = chats_util::get_users(user_id as i64); let resp = CommunicationValue::new(CommunicationType::get_chats) .with_id(cv.get_id()) .with_receiver(user_id) - .add_data(DataTypes::user_ids, DataValue::Number(users)); + .add_data(DataTypes::user_ids, DataValue::Str(users.dump())); self.send_message(&resp).await; return; } diff --git a/src/util/chat_files.rs b/src/util/chat_files.rs index 347a518..de6013b 100644 --- a/src/util/chat_files.rs +++ b/src/util/chat_files.rs @@ -1,9 +1,8 @@ -use crate::util::file_util::{get_children, get_directory, load_file, save_file}; -use json::{self, JsonValue, array, object}; -use std::fs::{self}; -use std::path::Path; - use crate::log; +use crate::util::file_util::get_directory; +use json::{JsonValue, array, object}; +use rusqlite::{Connection, params}; +use std::io; #[derive(PartialEq, Debug, Clone)] pub enum MessageState { @@ -22,14 +21,16 @@ impl MessageState { MessageState::Sending => "sending", } } - pub fn from_str(str: &str) -> Self { - match str.to_uppercase().as_str() { + + pub fn from_str(value: &str) -> Self { + match value.to_lowercase().as_str() { "read" => MessageState::Read, "received" => MessageState::Received, "sent" => MessageState::Sent, _ => MessageState::Sending, } } + pub fn upgrade(self, other: Self) -> Self { if other == Self::Read || self == Self::Read { Self::Read @@ -43,6 +44,34 @@ impl MessageState { } } +fn db_path() -> String { + format!("{}/messages.sqlite3", get_directory()) +} + +fn open_db() -> rusqlite::Result { + let conn = Connection::open(db_path())?; + conn.execute_batch( + r#" + PRAGMA journal_mode = WAL; + PRAGMA synchronous = NORMAL; + + CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + storage_owner INTEGER NOT NULL, + external_user INTEGER NOT NULL, + message_time INTEGER NOT NULL, + content TEXT NOT NULL, + sent_by_self INTEGER NOT NULL, + message_state TEXT NOT NULL + ); + + CREATE INDEX IF NOT EXISTS idx_messages_lookup + ON messages (storage_owner, external_user, message_time DESC); + "#, + )?; + Ok(conn) +} + pub fn add_message( send_time: u128, storage_owner_is_sender: bool, @@ -50,109 +79,103 @@ pub fn add_message( external_user: i64, message: &str, ) { - let user_dir = format!( - "{}/users/{}/chats/{}", - get_directory(), - storage_owner, - external_user - ); - - if let Err(e) = fs::create_dir_all(&user_dir) { - log!("Failed to create chat directory: {}", e); - return; - } - - let mut chunk_index = 0; - let mut message_chunk = array![]; - - // find latest chunk not full (max 800 msgs) - loop { - let file_name = format!("msgs_{}.json", chunk_index); - let file_content = load_file(&user_dir, &file_name); - - if !file_content.is_empty() { - if let Ok(current_chunk) = json::parse(&file_content) { - if current_chunk.is_array() && current_chunk.len() < 800 { - message_chunk = current_chunk; - break; - } - } else { - log!("Failed to parse existing JSON file: {}", file_name); - } - } else { - break; - } - - chunk_index += 1; - if chunk_index > 1000 { - log!("Too many message chunks. Aborting add."); + let message_time = match i64::try_from(send_time) { + Ok(v) => v, + Err(_) => { + log!("Failed to store message: send_time out of range for i64 ({send_time})"); return; } - } - - let json_obj = object! { - "timestamp" => send_time as i64, - "content" => message, - "sent_by_self" => storage_owner_is_sender, - "message_state" => MessageState::Sending.as_str() }; - if let Err(e) = message_chunk.push(json_obj) { - log!("Failed to push new message into JSON array: {}", e); - return; - } + let conn = match open_db() { + Ok(c) => c, + Err(e) => { + log!("Failed to open sqlite db for add_message: {}", e); + return; + } + }; - let file_name = format!("msgs_{}.json", chunk_index); - save_file(&user_dir, &file_name, &message_chunk.dump()); + if let Err(e) = conn.execute( + r#" + INSERT INTO messages ( + storage_owner, + external_user, + message_time, + content, + sent_by_self, + message_state + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6) + "#, + params![ + storage_owner, + external_user, + message_time, + message, + if storage_owner_is_sender { + 1_i64 + } else { + 0_i64 + }, + MessageState::Sending.as_str(), + ], + ) { + log!("Failed to insert message into sqlite: {}", e); + } } + pub fn change_message_state( timestamp: i64, storage_owner: i64, external_user: i64, new_state: MessageState, -) -> std::io::Result<()> { - let user_dir = format!("users/{}/chats/{}", storage_owner, external_user); - let path = Path::new(&user_dir); +) -> io::Result<()> { + let conn = open_db().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; - if !path.exists() { + let current: Option = match conn.query_row( + r#" + SELECT message_state + FROM messages + WHERE storage_owner = ?1 + AND external_user = ?2 + AND message_time = ?3 + ORDER BY id DESC + LIMIT 1 + "#, + params![storage_owner, external_user, timestamp], + |row| row.get(0), + ) { + Ok(state) => Some(state), + Err(rusqlite::Error::QueryReturnedNoRows) => None, + Err(e) => return Err(io::Error::new(io::ErrorKind::Other, e.to_string())), + }; + + let Some(current_state_raw) = current else { return Ok(()); - } + }; - let entries = fs::read_dir(path)?; - for entry in entries { - let entry = entry?; - let fname = entry.file_name(); - let fname_str = fname.to_string_lossy(); + let upgraded = MessageState::from_str(¤t_state_raw) + .upgrade(new_state) + .as_str() + .to_string(); - if fname_str.starts_with("msgs_") && fname_str.ends_with(".json") { - let file_content = load_file(&user_dir, &fname_str); - if file_content.is_empty() { - continue; - } + conn.execute( + r#" + UPDATE messages + SET message_state = ?1 + WHERE id = ( + SELECT id + FROM messages + WHERE storage_owner = ?2 + AND external_user = ?3 + AND message_time = ?4 + ORDER BY id DESC + LIMIT 1 + ) + "#, + params![upgraded, storage_owner, external_user, timestamp], + ) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; - if let Ok(mut chunk) = json::parse(&file_content) { - let mut modified = false; - for i in 0..chunk.len() { - if chunk[i]["message_time"].as_i64() == Some(timestamp) { - chunk[i]["message_state"] = JsonValue::from( - MessageState::from_str( - chunk[i]["message_state"].as_str().unwrap_or("SENDING"), - ) - .upgrade(new_state.clone()) - .as_str(), - ); - modified = true; - break; - } - } - - if modified { - save_file(&user_dir, &fname_str, &chunk.dump()); - break; - } - } - } - } Ok(()) } @@ -164,56 +187,105 @@ pub fn get_messages( ) -> JsonValue { let mut messages = array![]; - let mut latest_chunk_index: i32 = -1; - let files = get_children(&format!("users/{}/chats/{}", storage_owner, external_user)); - - for entry in files { - if let Some(num) = { - entry - .strip_prefix("msgs_") - .and_then(|s| s.strip_suffix(".json")) - } { - if let Ok(index) = num.parse::() { - if index > latest_chunk_index { - latest_chunk_index = index; - } - } - } - } - - if latest_chunk_index == -1 { + if amount <= 0 || loaded_messages < 0 { return messages; } - let mut to_skip = loaded_messages; - let mut needed = amount; + let conn = match open_db() { + Ok(c) => c, + Err(e) => { + log!("Failed to open sqlite db for get_messages: {}", e); + return messages; + } + }; - for chunk_index in (0..=latest_chunk_index).rev() { - if needed == 0 { - break; + let mut stmt = match conn.prepare( + r#" + SELECT + message_time, + content, + sent_by_self, + message_state + FROM messages + WHERE storage_owner = ?1 + AND external_user = ?2 + ORDER BY message_time DESC, id DESC + LIMIT ?3 OFFSET ?4 + "#, + ) { + Ok(s) => s, + Err(e) => { + log!("Failed to prepare get_messages query: {}", e); + return messages; } - let file_name = format!("msgs_{}.json", chunk_index); - let file_content = load_file( - &format!("users/{}/chats/{}", storage_owner, external_user), - &file_name, - ); - if file_content.is_empty() { - continue; + }; + + let rows = stmt.query_map( + params![storage_owner, external_user, amount, loaded_messages], + |row| { + let message_time: i64 = row.get(0)?; + let content: String = row.get(1)?; + let sent_by_self: i64 = row.get(2)?; + let message_state: String = row.get(3)?; + Ok((message_time, content, sent_by_self, message_state)) + }, + ); + + let Ok(rows) = rows else { + if let Err(e) = rows { + log!("Failed to query messages: {}", e); } - if let Ok(chunk) = json::parse(&file_content) { - for i in (0..chunk.len()).rev() { - if needed == 0 { - break; + return messages; + }; + + for row in rows { + match row { + Ok((message_time, content, sent_by_self, message_state)) => { + let msg = object! { + "message_time" => message_time, + "content" => content, + "sent_by_self" => (sent_by_self != 0), + "message_state" => message_state + }; + + if let Err(e) = messages.push(msg) { + log!("Failed to append message to output array: {}", e); } - if to_skip > 0 { - to_skip -= 1; - continue; - } - messages.push(chunk[i].clone()).unwrap(); - needed -= 1; + } + Err(e) => { + log!("Failed to read row from sqlite: {}", e); } } } messages } + +#[cfg(test)] +mod tests { + use super::MessageState; + + #[test] + fn upgrade_prefers_highest_state() { + assert_eq!( + MessageState::Sending.upgrade(MessageState::Sent), + MessageState::Sent + ); + assert_eq!( + MessageState::Sent.upgrade(MessageState::Received), + MessageState::Received + ); + assert_eq!( + MessageState::Received.upgrade(MessageState::Read), + MessageState::Read + ); + } + + #[test] + fn from_str_is_case_insensitive() { + assert_eq!(MessageState::from_str("READ"), MessageState::Read); + assert_eq!(MessageState::from_str("received"), MessageState::Received); + assert_eq!(MessageState::from_str("Sent"), MessageState::Sent); + assert_eq!(MessageState::from_str("unknown"), MessageState::Sending); + } +} diff --git a/src/util/chats_util.rs b/src/util/chats_util.rs index 73f1a00..00d1858 100644 --- a/src/util/chats_util.rs +++ b/src/util/chats_util.rs @@ -1,60 +1,134 @@ -use json::{self, JsonValue, array}; - use crate::users::contact::Contact; -use crate::util::file_util::{load_file, save_file}; +use crate::util::file_util::get_directory; +use json::{JsonValue, array}; +use rusqlite::{Connection, params}; + +fn db_path() -> String { + format!("{}/messages.sqlite3", get_directory()) +} + +fn open_db() -> rusqlite::Result { + let conn = Connection::open(db_path())?; + conn.execute_batch( + r#" + PRAGMA journal_mode = WAL; + PRAGMA synchronous = NORMAL; + + CREATE TABLE IF NOT EXISTS contacts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + storage_owner INTEGER NOT NULL, + user_id INTEGER NOT NULL, + user_name TEXT, + last_message_at INTEGER, + UNIQUE(storage_owner, user_id) + ); + + CREATE INDEX IF NOT EXISTS idx_contacts_owner + ON contacts (storage_owner, last_message_at DESC, user_id ASC); + "#, + )?; + Ok(conn) +} pub fn mod_user(storage_owner: i64, contact: &Contact) { - let dir: &str = &format!("users/{}/contacts/", storage_owner); - let s = load_file(dir, "contacts.json"); - - let mut contacts = if !s.is_empty() { - json::parse(&s).unwrap_or(array![]) - } else { - array![] + let conn = match open_db() { + Ok(c) => c, + Err(_) => return, }; - for i in 0..contacts.len() { - if contacts[i]["user_id"] == contact.user_id { - contacts.array_remove(i); - break; - } - } - - contacts.push(contact.to_json()).unwrap(); - save_file(&dir, "contacts.json", &contacts.dump()); + let _ = conn.execute( + r#" + INSERT INTO contacts ( + storage_owner, + user_id, + user_name, + last_message_at + ) VALUES (?1, ?2, ?3, ?4) + ON CONFLICT(storage_owner, user_id) DO UPDATE SET + user_name = excluded.user_name, + last_message_at = excluded.last_message_at + "#, + params![ + storage_owner, + contact.user_id, + contact.user_name.clone(), + contact.last_message_at + ], + ); } pub fn get_user(storage_owner: i64, user_id: i64) -> Option { - let dir = format!("users/{}/contacts/", storage_owner); - let s = load_file(&dir, "contacts.json"); - if s.is_empty() { - return None; - } + let conn = open_db().ok()?; - if let Ok(contacts) = json::parse(&s) { - for i in 0..contacts.len() { - if let Some(uid) = contacts[i]["user_id"].as_i64() { - if uid == user_id { - return Option::from(Contact::from_json(&contacts[i])); - } - } - } + let row = conn.query_row( + r#" + SELECT user_id, user_name, last_message_at + FROM contacts + WHERE storage_owner = ?1 AND user_id = ?2 + LIMIT 1 + "#, + params![storage_owner, user_id], + |r| { + let user_id: i64 = r.get(0)?; + let user_name: Option = r.get(1)?; + let last_message_at: Option = r.get(2)?; + Ok(Contact { + user_id, + user_name, + last_message_at, + }) + }, + ); + + match row { + Ok(contact) => Some(contact), + Err(rusqlite::Error::QueryReturnedNoRows) => None, + Err(_) => None, } - None } pub fn get_users(storage_owner: i64) -> JsonValue { - let dir: &str = &format!("users/{}/contacts/", storage_owner); - let s = load_file(dir, "contacts.json"); - let mut contacts_out = array![]; - if !s.is_empty() { - if let Ok(contacts) = json::parse(&s) { - for i in 0..contacts.len() { - let c = Contact::from_json(&contacts[i]); - contacts_out.push(c.to_json()).unwrap(); - } + + let conn = match open_db() { + Ok(c) => c, + Err(_) => return contacts_out, + }; + + let mut stmt = match conn.prepare( + r#" + SELECT user_id, user_name, last_message_at + FROM contacts + WHERE storage_owner = ?1 + ORDER BY + CASE WHEN last_message_at IS NULL THEN 1 ELSE 0 END, + last_message_at DESC, + user_id ASC + "#, + ) { + Ok(s) => s, + Err(_) => return contacts_out, + }; + + let rows = match stmt.query_map(params![storage_owner], |r| { + let user_id: i64 = r.get(0)?; + let user_name: Option = r.get(1)?; + let last_message_at: Option = r.get(2)?; + Ok(Contact { + user_id, + user_name, + last_message_at, + }) + }) { + Ok(r) => r, + Err(_) => return contacts_out, + }; + + for row in rows { + if let Ok(contact) = row { + let _ = contacts_out.push(contact.to_json()); } } + contacts_out }