[Add] Basic SQLite Implementation

This commit is contained in:
Alex Emmet 2026-03-16 23:38:37 +01:00
commit c14864c8b7
6 changed files with 415 additions and 200 deletions

109
Cargo.lock generated
View file

@ -552,9 +552,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.56" version = "1.2.57"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"jobserver", "jobserver",
@ -982,7 +982,7 @@ dependencies = [
[[package]] [[package]]
name = "epsilon-core" name = "epsilon-core"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/Tensamin/Epsilon.git#809a32581843e663d35d4b3222f2cc59371d2786" source = "git+https://github.com/Tensamin/Epsilon.git#d5edc693a91954834a38c39517b79092983eac5c"
dependencies = [ dependencies = [
"base64", "base64",
"byteorder", "byteorder",
@ -994,7 +994,7 @@ dependencies = [
[[package]] [[package]]
name = "epsilon-native" name = "epsilon-native"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/Tensamin/Epsilon.git#809a32581843e663d35d4b3222f2cc59371d2786" source = "git+https://github.com/Tensamin/Epsilon.git#d5edc693a91954834a38c39517b79092983eac5c"
dependencies = [ dependencies = [
"epsilon-core", "epsilon-core",
"quinn", "quinn",
@ -1030,6 +1030,18 @@ dependencies = [
"num-traits", "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]] [[package]]
name = "fancy-regex" name = "fancy-regex"
version = "0.11.0" version = "0.11.0"
@ -1358,6 +1370,15 @@ dependencies = [
"foldhash 0.2.0", "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]] [[package]]
name = "headers" name = "headers"
version = "0.4.1" version = "0.4.1"
@ -1714,9 +1735,9 @@ dependencies = [
[[package]] [[package]]
name = "instability" name = "instability"
version = "0.3.11" version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d" checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971"
dependencies = [ dependencies = [
"darling", "darling",
"indoc", "indoc",
@ -1753,6 +1774,7 @@ dependencies = [
"rand_core 0.6.4", "rand_core 0.6.4",
"ratatui", "ratatui",
"reqwest", "reqwest",
"rusqlite",
"rustls", "rustls",
"rustls-pemfile", "rustls-pemfile",
"serde_json", "serde_json",
@ -1879,9 +1901,9 @@ checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd"
[[package]] [[package]]
name = "kasuari" name = "kasuari"
version = "0.4.11" version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fe90c1150662e858c7d5f945089b7517b0a80d8bf7ba4b1b5ffc984e7230a5b" checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899"
dependencies = [ dependencies = [
"hashbrown 0.16.1", "hashbrown 0.16.1",
"portable-atomic", "portable-atomic",
@ -1924,6 +1946,16 @@ version = "0.2.183"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" 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]] [[package]]
name = "line-clipping" name = "line-clipping"
version = "0.3.5" version = "0.3.5"
@ -2228,9 +2260,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.3" version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]] [[package]]
name = "opaque-debug" name = "opaque-debug"
@ -2251,9 +2283,9 @@ dependencies = [
[[package]] [[package]]
name = "openssl" name = "openssl"
version = "0.10.75" version = "0.10.76"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf"
dependencies = [ dependencies = [
"bitflags 2.11.0", "bitflags 2.11.0",
"cfg-if", "cfg-if",
@ -2283,9 +2315,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]] [[package]]
name = "openssl-sys" name = "openssl-sys"
version = "0.9.111" version = "0.9.112"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",
@ -2984,6 +3016,31 @@ dependencies = [
"windows-sys 0.52.0", "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]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "2.1.1" version = "2.1.1"
@ -3130,9 +3187,9 @@ dependencies = [
[[package]] [[package]]
name = "schannel" name = "schannel"
version = "0.1.28" version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
dependencies = [ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
@ -3336,6 +3393,18 @@ dependencies = [
"windows-sys 0.61.2", "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]] [[package]]
name = "stable_deref_trait" name = "stable_deref_trait"
version = "1.2.1" version = "1.2.1"
@ -3478,9 +3547,9 @@ dependencies = [
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.26.0" version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [ dependencies = [
"fastrand", "fastrand",
"getrandom 0.4.2", "getrandom 0.4.2",
@ -3637,9 +3706,9 @@ dependencies = [
[[package]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.10.0" version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
dependencies = [ dependencies = [
"tinyvec_macros", "tinyvec_macros",
] ]

View file

@ -52,3 +52,4 @@ ratatui = "0.30.0"
open = "5.3.3" open = "5.3.3"
chrono = "0.4.43" chrono = "0.4.43"
serde_json = "1.0.149" serde_json = "1.0.149"
rusqlite = "0.39.0"

View file

@ -20,7 +20,7 @@ use crate::gui::app_state::AppState;
use crate::gui::screens::main_screen::MainScreen; use crate::gui::screens::main_screen::MainScreen;
use crate::gui::ui::start_tui; use crate::gui::ui::start_tui;
use crate::langu::language_creator; 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::terms::consent_state;
use crate::users::user_manager; use crate::users::user_manager;
use crate::util::config_util::CONFIG; use crate::util::config_util::CONFIG;

View file

@ -604,12 +604,11 @@ impl OmikronConnection {
let partner_id = cv.get_data(DataTypes::user_id).as_number().unwrap_or(0); 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 offset = cv.get_data(DataTypes::offset).as_number().unwrap_or(0);
let amount = cv.get_data(DataTypes::amount).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) let resp = CommunicationValue::new(CommunicationType::messages_get)
.with_id(cv.get_id()) .with_id(cv.get_id())
.with_receiver(my_id) .with_receiver(my_id)
//.add_data(DataTypes::messages, messages) .add_data(DataTypes::messages, DataValue::Str(messages.dump()));
;
self.send_message(&resp).await; self.send_message(&resp).await;
return; return;
@ -617,11 +616,11 @@ impl OmikronConnection {
if cv.is_type(CommunicationType::get_chats) { if cv.is_type(CommunicationType::get_chats) {
let user_id = cv.get_sender(); 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) let resp = CommunicationValue::new(CommunicationType::get_chats)
.with_id(cv.get_id()) .with_id(cv.get_id())
.with_receiver(user_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; self.send_message(&resp).await;
return; return;
} }

View file

@ -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::log;
use crate::util::file_util::get_directory;
use json::{JsonValue, array, object};
use rusqlite::{Connection, params};
use std::io;
#[derive(PartialEq, Debug, Clone)] #[derive(PartialEq, Debug, Clone)]
pub enum MessageState { pub enum MessageState {
@ -22,14 +21,16 @@ impl MessageState {
MessageState::Sending => "sending", 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, "read" => MessageState::Read,
"received" => MessageState::Received, "received" => MessageState::Received,
"sent" => MessageState::Sent, "sent" => MessageState::Sent,
_ => MessageState::Sending, _ => MessageState::Sending,
} }
} }
pub fn upgrade(self, other: Self) -> Self { pub fn upgrade(self, other: Self) -> Self {
if other == Self::Read || self == Self::Read { if other == Self::Read || self == Self::Read {
Self::Read Self::Read
@ -43,6 +44,34 @@ impl MessageState {
} }
} }
fn db_path() -> String {
format!("{}/messages.sqlite3", get_directory())
}
fn open_db() -> rusqlite::Result<Connection> {
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( pub fn add_message(
send_time: u128, send_time: u128,
storage_owner_is_sender: bool, storage_owner_is_sender: bool,
@ -50,109 +79,103 @@ pub fn add_message(
external_user: i64, external_user: i64,
message: &str, message: &str,
) { ) {
let user_dir = format!( let message_time = match i64::try_from(send_time) {
"{}/users/{}/chats/{}", Ok(v) => v,
get_directory(), Err(_) => {
storage_owner, log!("Failed to store message: send_time out of range for i64 ({send_time})");
external_user
);
if let Err(e) = fs::create_dir_all(&user_dir) {
log!("Failed to create chat directory: {}", e);
return; 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.");
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) { let conn = match open_db() {
log!("Failed to push new message into JSON array: {}", e); Ok(c) => c,
Err(e) => {
log!("Failed to open sqlite db for add_message: {}", e);
return; return;
} }
};
let file_name = format!("msgs_{}.json", chunk_index); if let Err(e) = conn.execute(
save_file(&user_dir, &file_name, &message_chunk.dump()); 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( pub fn change_message_state(
timestamp: i64, timestamp: i64,
storage_owner: i64, storage_owner: i64,
external_user: i64, external_user: i64,
new_state: MessageState, new_state: MessageState,
) -> std::io::Result<()> { ) -> io::Result<()> {
let user_dir = format!("users/{}/chats/{}", storage_owner, external_user); let conn = open_db().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
let path = Path::new(&user_dir);
if !path.exists() { let current: Option<String> = 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(()); return Ok(());
} };
let entries = fs::read_dir(path)?; let upgraded = MessageState::from_str(&current_state_raw)
for entry in entries { .upgrade(new_state)
let entry = entry?; .as_str()
let fname = entry.file_name(); .to_string();
let fname_str = fname.to_string_lossy();
if fname_str.starts_with("msgs_") && fname_str.ends_with(".json") { conn.execute(
let file_content = load_file(&user_dir, &fname_str); r#"
if file_content.is_empty() { UPDATE messages
continue; SET message_state = ?1
} WHERE id = (
SELECT id
if let Ok(mut chunk) = json::parse(&file_content) { FROM messages
let mut modified = false; WHERE storage_owner = ?2
for i in 0..chunk.len() { AND external_user = ?3
if chunk[i]["message_time"].as_i64() == Some(timestamp) { AND message_time = ?4
chunk[i]["message_state"] = JsonValue::from( ORDER BY id DESC
MessageState::from_str( LIMIT 1
chunk[i]["message_state"].as_str().unwrap_or("SENDING"),
) )
.upgrade(new_state.clone()) "#,
.as_str(), params![upgraded, storage_owner, external_user, timestamp],
); )
modified = true; .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
break;
}
}
if modified {
save_file(&user_dir, &fname_str, &chunk.dump());
break;
}
}
}
}
Ok(()) Ok(())
} }
@ -164,56 +187,105 @@ pub fn get_messages(
) -> JsonValue { ) -> JsonValue {
let mut messages = array![]; let mut messages = array![];
let mut latest_chunk_index: i32 = -1; if amount <= 0 || loaded_messages < 0 {
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::<i32>() {
if index > latest_chunk_index {
latest_chunk_index = index;
}
}
}
}
if latest_chunk_index == -1 {
return messages; return messages;
} }
let mut to_skip = loaded_messages; let conn = match open_db() {
let mut needed = amount; 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() { let mut stmt = match conn.prepare(
if needed == 0 { r#"
break; 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), let rows = stmt.query_map(
&file_name, 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))
},
); );
if file_content.is_empty() {
continue; let Ok(rows) = rows else {
if let Err(e) = rows {
log!("Failed to query messages: {}", e);
} }
if let Ok(chunk) = json::parse(&file_content) { return messages;
for i in (0..chunk.len()).rev() { };
if needed == 0 {
break; 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(); Err(e) => {
needed -= 1; log!("Failed to read row from sqlite: {}", e);
} }
} }
} }
messages 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);
}
}

View file

@ -1,60 +1,134 @@
use json::{self, JsonValue, array};
use crate::users::contact::Contact; 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<Connection> {
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) { pub fn mod_user(storage_owner: i64, contact: &Contact) {
let dir: &str = &format!("users/{}/contacts/", storage_owner); let conn = match open_db() {
let s = load_file(dir, "contacts.json"); Ok(c) => c,
Err(_) => return,
let mut contacts = if !s.is_empty() {
json::parse(&s).unwrap_or(array![])
} else {
array![]
}; };
for i in 0..contacts.len() { let _ = conn.execute(
if contacts[i]["user_id"] == contact.user_id { r#"
contacts.array_remove(i); INSERT INTO contacts (
break; storage_owner,
} user_id,
} user_name,
last_message_at
contacts.push(contact.to_json()).unwrap(); ) VALUES (?1, ?2, ?3, ?4)
save_file(&dir, "contacts.json", &contacts.dump()); 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<Contact> { pub fn get_user(storage_owner: i64, user_id: i64) -> Option<Contact> {
let dir = format!("users/{}/contacts/", storage_owner); let conn = open_db().ok()?;
let s = load_file(&dir, "contacts.json");
if s.is_empty() {
return None;
}
if let Ok(contacts) = json::parse(&s) { let row = conn.query_row(
for i in 0..contacts.len() { r#"
if let Some(uid) = contacts[i]["user_id"].as_i64() { SELECT user_id, user_name, last_message_at
if uid == user_id { FROM contacts
return Option::from(Contact::from_json(&contacts[i])); 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<String> = r.get(1)?;
let last_message_at: Option<i64> = 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 { 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![]; let mut contacts_out = array![];
if !s.is_empty() {
if let Ok(contacts) = json::parse(&s) { let conn = match open_db() {
for i in 0..contacts.len() { Ok(c) => c,
let c = Contact::from_json(&contacts[i]); Err(_) => return contacts_out,
contacts_out.push(c.to_json()).unwrap(); };
}
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<String> = r.get(1)?;
let last_message_at: Option<i64> = 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 contacts_out
} }