[Mig] storage to SQLite pool
[Clean] split message handler dispatch
This commit is contained in:
parent
9b20d159b5
commit
3be1d9f308
18 changed files with 1949 additions and 1700 deletions
39
Cargo.lock
generated
39
Cargo.lock
generated
|
|
@ -568,9 +568,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.12.0"
|
version = "1.12.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
|
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytestring"
|
name = "bytestring"
|
||||||
|
|
@ -2179,6 +2179,7 @@ dependencies = [
|
||||||
"json",
|
"json",
|
||||||
"mtp",
|
"mtp",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"r2d2",
|
||||||
"rand 0.8.6",
|
"rand 0.8.6",
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
|
|
@ -2186,8 +2187,10 @@ dependencies = [
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serde_yaml",
|
||||||
"sha2 0.10.9",
|
"sha2 0.10.9",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"uuid",
|
"uuid",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
|
|
@ -2548,9 +2551,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.8.2"
|
version = "2.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
|
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memmem"
|
name = "memmem"
|
||||||
|
|
@ -3535,6 +3538,17 @@ version = "6.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r2d2"
|
||||||
|
version = "0.8.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"parking_lot",
|
||||||
|
"scheduled-thread-pool",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.8.6"
|
version = "0.8.6"
|
||||||
|
|
@ -4016,6 +4030,15 @@ dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scheduled-thread-pool"
|
||||||
|
version = "0.2.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19"
|
||||||
|
dependencies = [
|
||||||
|
"parking_lot",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scoped-tls"
|
name = "scoped-tls"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
|
@ -5628,18 +5651,18 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.53"
|
version = "0.8.54"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "75726053136156d419e285b9b7eddaaea9e3fea6ce32eed44a89901f0bd98de1"
|
checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerocopy-derive",
|
"zerocopy-derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy-derive"
|
name = "zerocopy-derive"
|
||||||
version = "0.8.53"
|
version = "0.8.54"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4714fd92cf900833d49538023a9b3915155210801d1c1169eba513b2addefd71"
|
checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
|
||||||
|
|
@ -512,24 +512,18 @@ impl ClientConnection {
|
||||||
let messages = chat_files::get_messages(user_id, contact.user_id, 0, amount);
|
let messages = chat_files::get_messages(user_id, contact.user_id, 0, amount);
|
||||||
|
|
||||||
let mut msg_array = Vec::new();
|
let mut msg_array = Vec::new();
|
||||||
for m in messages.members() {
|
for m in &messages {
|
||||||
let message_time = m["message_time"].as_i64().unwrap_or(0);
|
|
||||||
let content = m["content"].as_str().unwrap_or("").to_string();
|
|
||||||
let sent_by_self = m["sent_by_self"].as_bool().unwrap_or(false);
|
|
||||||
let height = m["height"].as_i64().unwrap_or(0);
|
|
||||||
let message_state = m["message_state"].as_str().unwrap_or("").to_string();
|
|
||||||
|
|
||||||
let mut msg_container = Vec::new();
|
let mut msg_container = Vec::new();
|
||||||
msg_container.push((
|
msg_container.push((
|
||||||
DataType::SendTime,
|
DataType::SendTime,
|
||||||
DataValue::SignedNumber(message_time as i128),
|
DataValue::SignedNumber(m.message_time as i128),
|
||||||
));
|
));
|
||||||
msg_container.push((DataType::Content, DataValue::Str(content.clone())));
|
msg_container.push((DataType::Content, DataValue::Str(m.content.clone())));
|
||||||
msg_container.push((DataType::MessageState, DataValue::Str(message_state)));
|
msg_container.push((DataType::MessageState, DataValue::Str(m.message_state.clone())));
|
||||||
msg_container.push((DataType::Height, DataValue::SignedNumber(height as i128)));
|
msg_container.push((DataType::Height, DataValue::SignedNumber(m.height as i128)));
|
||||||
msg_container.push((
|
msg_container.push((
|
||||||
DataType::SenderId,
|
DataType::SenderId,
|
||||||
DataValue::UnsignedNumber(if sent_by_self {
|
DataValue::UnsignedNumber(if m.sent_by_self {
|
||||||
user_id as u128
|
user_id as u128
|
||||||
} else {
|
} else {
|
||||||
contact.user_id as u128
|
contact.user_id as u128
|
||||||
|
|
@ -538,13 +532,13 @@ impl ClientConnection {
|
||||||
msg_array.push(typed_container(msg_container));
|
msg_array.push(typed_container(msg_container));
|
||||||
|
|
||||||
if msg_array.len() == 1 {
|
if msg_array.len() == 1 {
|
||||||
let sender_id = if sent_by_self {
|
let sender_id = if m.sent_by_self {
|
||||||
user_id
|
user_id
|
||||||
} else {
|
} else {
|
||||||
contact.user_id
|
contact.user_id
|
||||||
};
|
};
|
||||||
let mut last_msg = Vec::new();
|
let mut last_msg = Vec::new();
|
||||||
last_msg.push((DataType::Content, DataValue::Str(content)));
|
last_msg.push((DataType::Content, DataValue::Str(m.content.clone())));
|
||||||
last_msg.push((
|
last_msg.push((
|
||||||
DataType::SenderId,
|
DataType::SenderId,
|
||||||
DataValue::SignedNumber(sender_id as i128),
|
DataValue::SignedNumber(sender_id as i128),
|
||||||
|
|
@ -749,12 +743,8 @@ impl ClientConnection {
|
||||||
amount as i64,
|
amount as i64,
|
||||||
);
|
);
|
||||||
let mut msg_array: Vec<DataValue> = Vec::new();
|
let mut msg_array: Vec<DataValue> = Vec::new();
|
||||||
for m in messages.members() {
|
for m in &messages {
|
||||||
let message_time: i64 = m["message_time"].as_i64().unwrap_or(0);
|
let sender_id: i64 = if m.sent_by_self {
|
||||||
let content: String = m["content"].as_str().unwrap_or("").to_string();
|
|
||||||
let sent_by_self: bool = m["sent_by_self"].as_bool().unwrap_or(false);
|
|
||||||
let height: i64 = m["height"].as_i64().unwrap_or(0);
|
|
||||||
let sender_id: i64 = if sent_by_self {
|
|
||||||
my_id as i64
|
my_id as i64
|
||||||
} else {
|
} else {
|
||||||
if let Some(n) = cv.get_data(DataType::ChatPartnerId).as_number() {
|
if let Some(n) = cv.get_data(DataType::ChatPartnerId).as_number() {
|
||||||
|
|
@ -765,23 +755,22 @@ impl ClientConnection {
|
||||||
partner_id as i64
|
partner_id as i64
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let message_state: String = m["message_state"].as_str().unwrap_or("").to_string();
|
|
||||||
|
|
||||||
let mut container = Vec::new();
|
let mut container = Vec::new();
|
||||||
container.push((
|
container.push((
|
||||||
DataType::SendTime,
|
DataType::SendTime,
|
||||||
DataValue::SignedNumber(message_time as i128),
|
DataValue::SignedNumber(m.message_time as i128),
|
||||||
));
|
));
|
||||||
container.push((DataType::Content, DataValue::Str(content)));
|
container.push((DataType::Content, DataValue::Str(m.content.clone())));
|
||||||
container.push((
|
container.push((
|
||||||
DataType::SenderId,
|
DataType::SenderId,
|
||||||
DataValue::SignedNumber(sender_id as i128),
|
DataValue::SignedNumber(sender_id as i128),
|
||||||
));
|
));
|
||||||
container.push((DataType::MessageState, DataValue::Str(message_state)));
|
container.push((DataType::MessageState, DataValue::Str(m.message_state.clone())));
|
||||||
container.push((DataType::Height, DataValue::SignedNumber(height as i128)));
|
container.push((DataType::Height, DataValue::SignedNumber(m.height as i128)));
|
||||||
container.push((
|
container.push((
|
||||||
DataType::SenderId,
|
DataType::SenderId,
|
||||||
DataValue::UnsignedNumber(if sent_by_self {
|
DataValue::UnsignedNumber(if m.sent_by_self {
|
||||||
my_id as u128
|
my_id as u128
|
||||||
} else {
|
} else {
|
||||||
partner_id as u128
|
partner_id as u128
|
||||||
|
|
@ -883,18 +872,12 @@ impl ClientConnection {
|
||||||
let mut comm_array = Vec::new();
|
let mut comm_array = Vec::new();
|
||||||
for c in CommunitiesUtil::get_communities(cv.get_sender() as i64) {
|
for c in CommunitiesUtil::get_communities(cv.get_sender() as i64) {
|
||||||
let mut container: Vec<(DataType, DataValue)> = Vec::new();
|
let mut container: Vec<(DataType, DataValue)> = Vec::new();
|
||||||
if let Some(address) = c["address"].as_str() {
|
|
||||||
container.push((
|
container.push((
|
||||||
DataType::CommunityAddress,
|
DataType::CommunityAddress,
|
||||||
DataValue::Str(address.to_string()),
|
DataValue::Str(c.address.clone()),
|
||||||
));
|
));
|
||||||
}
|
container.push((DataType::CommunityTitle, DataValue::Str(c.title.clone())));
|
||||||
if let Some(title) = c["title"].as_str() {
|
container.push((DataType::Position, DataValue::Str(c.position.clone())));
|
||||||
container.push((DataType::CommunityTitle, DataValue::Str(title.to_string())));
|
|
||||||
}
|
|
||||||
if let Some(position) = c["position"].as_str() {
|
|
||||||
container.push((DataType::Position, DataValue::Str(position.to_string())));
|
|
||||||
}
|
|
||||||
comm_array.push(typed_container(container));
|
comm_array.push(typed_container(container));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -444,7 +444,7 @@ pub async fn run_command(command: &str) {
|
||||||
if let Some(user) = user_manager::get_user_by_username(username) {
|
if let Some(user) = user_manager::get_user_by_username(username) {
|
||||||
let msg = CommunicationValue::new(CommunicationType::DeleteUser)
|
let msg = CommunicationValue::new(CommunicationType::DeleteUser)
|
||||||
.with_sender(user.user_id as u64);
|
.with_sender(user.user_id as u64);
|
||||||
OMIKRON_CONNECTION.send_message(&msg).await;
|
let _ = OMIKRON_CONNECTION.send_message(&msg).await;
|
||||||
user_manager::remove_user(user.user_id);
|
user_manager::remove_user(user.user_id);
|
||||||
log!("Removed user {}", user.user_id);
|
log!("Removed user {}", user.user_id);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,11 @@ hkdf = "0.12.4"
|
||||||
json = "*"
|
json = "*"
|
||||||
arc-swap = "1"
|
arc-swap = "1"
|
||||||
once_cell = "1.21.3"
|
once_cell = "1.21.3"
|
||||||
|
r2d2 = "0.8"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
serde_yaml = "0.9"
|
||||||
|
thiserror = "2"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
rand_core = { version = "0.6", features = ["getrandom", "std"] }
|
rand_core = { version = "0.6", features = ["getrandom", "std"] }
|
||||||
ratatui = "0.30.0"
|
ratatui = "0.30.0"
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
|
pub mod storage_error;
|
||||||
pub mod users;
|
pub mod users;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
|
||||||
13
iota-storage/src/storage_error.rs
Normal file
13
iota-storage/src/storage_error.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum StorageError {
|
||||||
|
#[error("Database error: {0}")]
|
||||||
|
Db(#[from] rusqlite::Error),
|
||||||
|
#[error("Connection pool error: {0}")]
|
||||||
|
Pool(String),
|
||||||
|
#[error("IO error: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
#[error("{0}")]
|
||||||
|
Other(String),
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
use json::{self, JsonValue, number::Number};
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -33,29 +32,4 @@ impl Contact {
|
||||||
pub fn set_last_message_at(&mut self, p0: i64) {
|
pub fn set_last_message_at(&mut self, p0: i64) {
|
||||||
self.last_message_at = Option::from(p0);
|
self.last_message_at = Option::from(p0);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_json(&self) -> JsonValue {
|
|
||||||
let mut obj = JsonValue::new_object();
|
|
||||||
obj["user_id"] = JsonValue::Number(Number::from(self.user_id));
|
|
||||||
if let Some(name) = &self.user_name {
|
|
||||||
obj["user_name"] = JsonValue::from(name.as_str());
|
|
||||||
}
|
|
||||||
if let Some(ts) = &self.last_message_at {
|
|
||||||
obj["last_message_at"] = JsonValue::Number(Number::from(*ts));
|
|
||||||
}
|
|
||||||
obj
|
|
||||||
}
|
|
||||||
pub fn from_json(o: &JsonValue) -> Contact {
|
|
||||||
let user_id = o["user_id"].as_i64().unwrap_or(0);
|
|
||||||
|
|
||||||
let user_name = o["user_name"].as_str().map(|s| s.to_string());
|
|
||||||
|
|
||||||
let last_message_at = o["last_message_at"].as_i64();
|
|
||||||
|
|
||||||
Contact {
|
|
||||||
user_id,
|
|
||||||
user_name,
|
|
||||||
last_message_at,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
pub mod contact;
|
pub mod contact;
|
||||||
pub mod user_community_util;
|
|
||||||
pub mod user_manager;
|
pub mod user_manager;
|
||||||
pub mod user_profile;
|
pub mod user_profile;
|
||||||
|
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
use iota_util::file_util::{load_file, save_file};
|
|
||||||
use json::{self, Array, JsonValue};
|
|
||||||
|
|
||||||
pub struct UserCommunityUtil;
|
|
||||||
|
|
||||||
impl UserCommunityUtil {
|
|
||||||
pub fn add_community(storage_owner: i64, address: String, title: String, position: String) {
|
|
||||||
let file_path = format!("users/{}/", storage_owner);
|
|
||||||
let mut communities = Self::load_array(&file_path, "communities.json");
|
|
||||||
|
|
||||||
let mut community = JsonValue::new_object();
|
|
||||||
community["title"] = JsonValue::String(title);
|
|
||||||
community["address"] = JsonValue::String(address);
|
|
||||||
community["position"] = JsonValue::String(position);
|
|
||||||
|
|
||||||
communities.push(community);
|
|
||||||
|
|
||||||
save_file(
|
|
||||||
&file_path,
|
|
||||||
"communities.json",
|
|
||||||
&JsonValue::Array(communities).to_string(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove_community(storage_owner: i64, community_address: String) {
|
|
||||||
let file_path = format!("users/{}/", storage_owner);
|
|
||||||
let communities = Self::load_array(&file_path, "communities.json");
|
|
||||||
|
|
||||||
let filtered: Array = communities
|
|
||||||
.iter()
|
|
||||||
.filter(|entry| entry["address"].as_str() != Some(&community_address))
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
save_file(
|
|
||||||
&file_path,
|
|
||||||
"communities.json",
|
|
||||||
&JsonValue::Array(filtered).to_string(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_communities(storage_owner: i64) -> Array {
|
|
||||||
let file_path = format!("users/{}/", storage_owner);
|
|
||||||
Self::load_array(&file_path, "communities.json")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_array(dir: &str, name: &str) -> Array {
|
|
||||||
let content = load_file(dir, name);
|
|
||||||
if content.is_empty() {
|
|
||||||
return Array::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
let parsed = json::parse(&content);
|
|
||||||
match parsed {
|
|
||||||
Ok(JsonValue::Array(arr)) => arr,
|
|
||||||
_ => Array::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +1,212 @@
|
||||||
use crate::users::user_profile::UserProfile;
|
use crate::users::user_profile::UserProfile;
|
||||||
|
use crate::util::db;
|
||||||
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
||||||
use iota_util::crypto_helper::{self, hex_hash, keyring_from_base64, public_key_bundle_to_base64};
|
use iota_util::crypto_helper::{self, hex_hash, keyring_from_base64, public_key_bundle_to_base64};
|
||||||
use iota_util::file_util::{load_file, save_file};
|
use iota_util::file_util::{load_file, save_file};
|
||||||
use json::JsonValue;
|
use rusqlite::params;
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use rand_core::{OsRng, RngCore};
|
use rand_core::{OsRng, RngCore};
|
||||||
use std::io::{self};
|
|
||||||
use std::sync::Mutex;
|
|
||||||
|
|
||||||
static USERS: Lazy<Mutex<Vec<UserProfile>>> = Lazy::new(|| Mutex::new(Vec::new()));
|
pub fn add_user(user: UserProfile) {
|
||||||
static UNIQUE: Lazy<Mutex<bool>> = Lazy::new(|| Mutex::new(false));
|
if let Err(e) = db::with_db(|conn| {
|
||||||
|
conn.execute(
|
||||||
|
r#"
|
||||||
|
INSERT INTO users (user_id, username, public_key, private_key_hash, reset_token, created_at, display_name)
|
||||||
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
|
||||||
|
ON CONFLICT(user_id) DO UPDATE SET
|
||||||
|
username = excluded.username,
|
||||||
|
public_key = excluded.public_key,
|
||||||
|
private_key_hash = excluded.private_key_hash,
|
||||||
|
reset_token = excluded.reset_token,
|
||||||
|
display_name = excluded.display_name
|
||||||
|
"#,
|
||||||
|
params![
|
||||||
|
user.user_id,
|
||||||
|
user.username,
|
||||||
|
user.public_key,
|
||||||
|
user.private_key_hash,
|
||||||
|
user.reset_token,
|
||||||
|
user.created_at,
|
||||||
|
user.display_name,
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
for (app_id, app_secret) in &user.trusted_apps {
|
||||||
|
conn.execute(
|
||||||
|
r#"
|
||||||
|
INSERT OR REPLACE INTO trusted_apps (user_id, app_id, app_secret)
|
||||||
|
VALUES (?1, ?2, ?3)
|
||||||
|
"#,
|
||||||
|
params![user.user_id, app_id, app_secret],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}) {
|
||||||
|
eprintln!("Failed to add_user: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_user(user: UserProfile) {
|
||||||
|
add_user(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_user_by_username(username: &str) -> Option<UserProfile> {
|
||||||
|
match db::with_db(|conn| {
|
||||||
|
match conn.query_row(
|
||||||
|
"SELECT user_id, username, public_key, private_key_hash, reset_token, created_at, display_name FROM users WHERE username = ?1 LIMIT 1",
|
||||||
|
params![username],
|
||||||
|
|r| {
|
||||||
|
let user_id: i64 = r.get(0)?;
|
||||||
|
Ok(UserProfile {
|
||||||
|
user_id,
|
||||||
|
username: r.get(1)?,
|
||||||
|
display_name: r.get(6)?,
|
||||||
|
public_key: r.get(2)?,
|
||||||
|
private_key_hash: r.get(3)?,
|
||||||
|
created_at: r.get(5)?,
|
||||||
|
reset_token: r.get(4)?,
|
||||||
|
trusted_apps: load_trusted_apps(user_id),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Ok(user) => Ok(Some(user)),
|
||||||
|
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Ok(opt) => opt,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error querying user by username: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_user(user_id: i64) -> Option<UserProfile> {
|
||||||
|
match db::with_db(|conn| {
|
||||||
|
match conn.query_row(
|
||||||
|
"SELECT user_id, username, public_key, private_key_hash, reset_token, created_at, display_name FROM users WHERE user_id = ?1 LIMIT 1",
|
||||||
|
params![user_id],
|
||||||
|
|r| {
|
||||||
|
let user_id: i64 = r.get(0)?;
|
||||||
|
Ok(UserProfile {
|
||||||
|
user_id,
|
||||||
|
username: r.get(1)?,
|
||||||
|
display_name: r.get(6)?,
|
||||||
|
public_key: r.get(2)?,
|
||||||
|
private_key_hash: r.get(3)?,
|
||||||
|
created_at: r.get(5)?,
|
||||||
|
reset_token: r.get(4)?,
|
||||||
|
trusted_apps: load_trusted_apps(user_id),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Ok(user) => Ok(Some(user)),
|
||||||
|
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Ok(opt) => opt,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error querying user: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_users() -> Vec<UserProfile> {
|
||||||
|
match db::with_db(|conn| {
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
r#"
|
||||||
|
SELECT user_id, username, public_key, private_key_hash, reset_token, created_at, display_name
|
||||||
|
FROM users
|
||||||
|
ORDER BY username
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let rows = stmt.query_map([], |r| {
|
||||||
|
let user_id: i64 = r.get(0)?;
|
||||||
|
let username: String = r.get(1)?;
|
||||||
|
let public_key: String = r.get(2)?;
|
||||||
|
let private_key_hash: String = r.get(3)?;
|
||||||
|
let reset_token: String = r.get(4)?;
|
||||||
|
let created_at: i64 = r.get(5)?;
|
||||||
|
let display_name: Option<String> = r.get(6)?;
|
||||||
|
|
||||||
|
Ok(UserProfile {
|
||||||
|
user_id,
|
||||||
|
username,
|
||||||
|
display_name,
|
||||||
|
public_key,
|
||||||
|
private_key_hash,
|
||||||
|
created_at,
|
||||||
|
reset_token,
|
||||||
|
trusted_apps: std::collections::HashMap::new(),
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for row in rows {
|
||||||
|
match row {
|
||||||
|
Ok(mut user) => {
|
||||||
|
user.trusted_apps = load_trusted_apps(user.user_id);
|
||||||
|
out.push(user);
|
||||||
|
}
|
||||||
|
Err(e) => eprintln!("Failed to read user row: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to query users: {}", e);
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_trusted_apps(user_id: i64) -> std::collections::HashMap<String, String> {
|
||||||
|
match db::with_db(|conn| {
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
"SELECT app_id, app_secret FROM trusted_apps WHERE user_id = ?1",
|
||||||
|
)?;
|
||||||
|
let rows = stmt.query_map(params![user_id], |r| {
|
||||||
|
Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut map = std::collections::HashMap::new();
|
||||||
|
for row in rows {
|
||||||
|
if let Ok((k, v)) = row {
|
||||||
|
map.insert(k, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(map)
|
||||||
|
}) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to load trusted apps: {}", e);
|
||||||
|
std::collections::HashMap::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_user(user_id: i64) {
|
||||||
|
if let Err(e) = db::with_db(|conn| {
|
||||||
|
conn.execute("DELETE FROM trusted_apps WHERE user_id = ?1", params![user_id])?;
|
||||||
|
conn.execute("DELETE FROM users WHERE user_id = ?1", params![user_id])?;
|
||||||
|
Ok(())
|
||||||
|
}) {
|
||||||
|
eprintln!("Failed to remove_user: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear() {
|
||||||
|
if let Err(e) = db::with_db(|conn| {
|
||||||
|
conn.execute_batch("DELETE FROM trusted_apps; DELETE FROM users;")?;
|
||||||
|
Ok(())
|
||||||
|
}) {
|
||||||
|
eprintln!("Failed to clear users: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub async fn load_from_tu(username: &str) -> Result<(), ()> {
|
pub async fn load_from_tu(username: &str) -> Result<(), ()> {
|
||||||
|
|
@ -35,91 +232,38 @@ pub async fn load_from_tu(username: &str) -> Result<(), ()> {
|
||||||
hex_hash(&keyring_b64),
|
hex_hash(&keyring_b64),
|
||||||
reset_token,
|
reset_token,
|
||||||
);
|
);
|
||||||
USERS.lock().unwrap().push(user_profile);
|
add_user(user_profile);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_user(user: UserProfile) {
|
|
||||||
USERS.lock().unwrap().push(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_user(user: UserProfile) {
|
|
||||||
let mut users = USERS.lock().unwrap();
|
|
||||||
if let Some(pos) = users.iter().position(|u| u.user_id == user.user_id) {
|
|
||||||
users[pos] = user;
|
|
||||||
}
|
|
||||||
*UNIQUE.lock().unwrap() = true;
|
|
||||||
}
|
|
||||||
pub fn get_user_by_username(username: &str) -> Option<UserProfile> {
|
|
||||||
USERS
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.find(|u| u.username == username)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_user(user_id: i64) -> Option<UserProfile> {
|
|
||||||
USERS
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.find(|u| u.user_id == user_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_users() -> Vec<UserProfile> {
|
|
||||||
USERS.lock().unwrap().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove_user(user_id: i64) {
|
|
||||||
let mut users = USERS.lock().unwrap();
|
|
||||||
users.retain(|u| u.user_id != user_id);
|
|
||||||
*UNIQUE.lock().unwrap() = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn save_users() {
|
pub fn save_users() {
|
||||||
*UNIQUE.lock().unwrap() = false;
|
// No-op: users are auto-saved via SQLite.
|
||||||
let users = USERS.lock().unwrap();
|
|
||||||
let arr: Vec<JsonValue> = users.iter().map(|u| u.to_json()).collect();
|
|
||||||
let json_str = JsonValue::Array(arr).dump();
|
|
||||||
|
|
||||||
save_file("", "users.json", &json_str);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear() {
|
pub async fn load_users() -> std::io::Result<()> {
|
||||||
let mut users = USERS.lock().unwrap();
|
// Users are loaded from SQLite on demand. This function is kept for API compat.
|
||||||
users.clear();
|
// If we need to migrate from a legacy users.json file, we can do so here.
|
||||||
*UNIQUE.lock().unwrap() = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn load_users() -> io::Result<()> {
|
|
||||||
let content = load_file("", "users.json");
|
let content = load_file("", "users.json");
|
||||||
if content.trim().is_empty() {
|
if content.trim().is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
if let Ok(parsed) = json::parse(&content) {
|
||||||
let parsed =
|
if let json::JsonValue::Array(arr) = parsed {
|
||||||
json::parse(&content).map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
|
|
||||||
if let JsonValue::Array(arr) = parsed {
|
|
||||||
let mut users = USERS.lock().unwrap();
|
|
||||||
for j in arr.iter() {
|
for j in arr.iter() {
|
||||||
if let Some(up) = UserProfile::from_json(j).await {
|
if let Some(up) = UserProfile::from_json(j) {
|
||||||
users.push(up);
|
add_user(up);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if *UNIQUE.lock().unwrap() {
|
|
||||||
save_users();
|
|
||||||
}
|
}
|
||||||
|
// Rename the old file so we don't re-import
|
||||||
|
let _ = std::fs::rename(
|
||||||
|
std::path::PathBuf::from(iota_util::file_util::get_directory()).join("users.json"),
|
||||||
|
std::path::PathBuf::from(iota_util::file_util::get_directory()).join("users.json.imported"),
|
||||||
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn set_unique(val: bool) {
|
|
||||||
*UNIQUE.lock().unwrap() = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn save_app_data(user_id: i64, app_identifier: &str, data: &str) {
|
pub fn save_app_data(user_id: i64, app_identifier: &str, data: &str) {
|
||||||
let path = format!("users/{}/apps", user_id);
|
let path = format!("users/{}/apps", user_id);
|
||||||
let name = format!("{}.json", app_identifier);
|
let name = format!("{}.json", app_identifier);
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@ use iota_util::file_util::{has_file, load_file, used_dir_space};
|
||||||
use json::{JsonValue, object};
|
use json::{JsonValue, object};
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use rand::rngs::OsRng;
|
use rand::rngs::OsRng;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
// --- UserProfile ---
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct UserProfile {
|
pub struct UserProfile {
|
||||||
pub user_id: i64,
|
pub user_id: i64,
|
||||||
pub username: String,
|
pub username: String,
|
||||||
|
|
@ -43,26 +43,6 @@ impl UserProfile {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_json(&self) -> JsonValue {
|
|
||||||
let mut trusted_apps_obj = json::JsonValue::new_object();
|
|
||||||
for (k, v) in &self.trusted_apps {
|
|
||||||
trusted_apps_obj[k] = v.clone().into();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut obj = object! {
|
|
||||||
"uuid" => self.user_id,
|
|
||||||
"username" => self.username.clone(),
|
|
||||||
"public_key" => self.public_key.clone(),
|
|
||||||
"private_key_hash" => self.private_key_hash.clone(),
|
|
||||||
"created_at" => self.created_at,
|
|
||||||
"reset_token" => self.reset_token.clone(),
|
|
||||||
"trusted_apps" => trusted_apps_obj,
|
|
||||||
};
|
|
||||||
if let Some(d) = &self.display_name {
|
|
||||||
obj["display_name"] = d.clone().into();
|
|
||||||
}
|
|
||||||
obj
|
|
||||||
}
|
|
||||||
pub fn frontend(&self) -> JsonValue {
|
pub fn frontend(&self) -> JsonValue {
|
||||||
let mut obj = object! {
|
let mut obj = object! {
|
||||||
"uuid" => self.user_id,
|
"uuid" => self.user_id,
|
||||||
|
|
@ -78,10 +58,11 @@ impl UserProfile {
|
||||||
if has_file("", &format!("{}.tu", self.username.clone())) {
|
if has_file("", &format!("{}.tu", self.username.clone())) {
|
||||||
obj["tu"] = load_file("", &format!("{}.tu", self.username.clone())).into();
|
obj["tu"] = load_file("", &format!("{}.tu", self.username.clone())).into();
|
||||||
}
|
}
|
||||||
|
|
||||||
obj
|
obj
|
||||||
}
|
}
|
||||||
pub async fn from_json(j: &JsonValue) -> Option<Self> {
|
|
||||||
|
/// Legacy JSON import - used when migrating from users.json to SQLite.
|
||||||
|
pub fn from_json(j: &JsonValue) -> Option<Self> {
|
||||||
let user_id = j["uuid"].as_i64()?;
|
let user_id = j["uuid"].as_i64()?;
|
||||||
let username = j["username"].as_str()?.to_string();
|
let username = j["username"].as_str()?.to_string();
|
||||||
let public_key = j["public_key"].as_str()?.to_string();
|
let public_key = j["public_key"].as_str()?.to_string();
|
||||||
|
|
@ -99,7 +80,7 @@ impl UserProfile {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let up = UserProfile {
|
Some(UserProfile {
|
||||||
user_id,
|
user_id,
|
||||||
username,
|
username,
|
||||||
display_name,
|
display_name,
|
||||||
|
|
@ -108,22 +89,15 @@ impl UserProfile {
|
||||||
created_at,
|
created_at,
|
||||||
reset_token,
|
reset_token,
|
||||||
trusted_apps,
|
trusted_apps,
|
||||||
};
|
})
|
||||||
|
|
||||||
// TODO: Migrate to Omikron / Wss
|
|
||||||
/* if j.has_key("migrate")
|
|
||||||
|| j.has_key("migrating")
|
|
||||||
|| j.has_key("changing")
|
|
||||||
|| j.has_key("move")
|
|
||||||
|| j.has_key("moving")
|
|
||||||
{
|
|
||||||
if auth_connector::migrate_user(&mut up).await {
|
|
||||||
log_message(format!("[INFO] Migration triggered for {}", up.username));
|
|
||||||
user_manager::set_unique(true);
|
|
||||||
}
|
}
|
||||||
} */
|
|
||||||
|
|
||||||
Some(up)
|
pub fn from_yaml(s: &str) -> Result<Self, serde_yaml::Error> {
|
||||||
|
serde_yaml::from_str(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
|
||||||
|
serde_yaml::to_string(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
use crate::util::db;
|
use crate::util::db;
|
||||||
use iota_logger::log;
|
use iota_logger::log;
|
||||||
use json::{JsonValue, array, object};
|
|
||||||
use rusqlite::params;
|
use rusqlite::params;
|
||||||
use std::io;
|
use crate::storage_error::StorageError;
|
||||||
use std::sync::{Arc, LazyLock, Mutex};
|
|
||||||
|
|
||||||
#[derive(PartialEq, Debug, Clone)]
|
#[derive(PartialEq, Debug, Clone)]
|
||||||
pub enum MessageState {
|
pub enum MessageState {
|
||||||
|
|
@ -45,11 +43,202 @@ impl MessageState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shared DB created via helper.
|
#[derive(Debug, Clone)]
|
||||||
// The db helper constructs the messages sqlite file and ensures PRAGMAs and schema exist.
|
pub struct StoredMessage {
|
||||||
static MESSAGES_DB: LazyLock<Arc<Mutex<rusqlite::Connection>>> = LazyLock::new(|| {
|
pub id: i64,
|
||||||
db::create_general_messages_db().expect("Failed to create or initialize general messages DB")
|
pub message_time: i64,
|
||||||
});
|
pub content: String,
|
||||||
|
pub edited: bool,
|
||||||
|
pub sent_by_self: bool,
|
||||||
|
pub message_state: String,
|
||||||
|
pub height: i64,
|
||||||
|
pub reply_to: Option<i64>,
|
||||||
|
pub reactions: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Each edit is recorded in message_edits with the before/after content and a
|
||||||
|
* timestamp. Only the original sender (sent_by_self = 1) may edit.
|
||||||
|
*/
|
||||||
|
pub fn edit_message(
|
||||||
|
storage_owner: i64,
|
||||||
|
external_user: i64,
|
||||||
|
message_time: i64,
|
||||||
|
editor_id: i64,
|
||||||
|
new_content: &str,
|
||||||
|
) -> Result<(), StorageError> {
|
||||||
|
db::with_db(|conn| {
|
||||||
|
let msg = conn.query_row(
|
||||||
|
r#"
|
||||||
|
SELECT id, content, sent_by_self
|
||||||
|
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, message_time],
|
||||||
|
|row| {
|
||||||
|
Ok((
|
||||||
|
row.get::<_, i64>(0)?,
|
||||||
|
row.get::<_, String>(1)?,
|
||||||
|
row.get::<_, i64>(2)?,
|
||||||
|
))
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let (msg_id, old_content, sent_by_self) = msg;
|
||||||
|
if sent_by_self != 1 {
|
||||||
|
return Err(StorageError::Other(
|
||||||
|
"Only the original sender can edit this message".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let now = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_millis() as i64;
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
r#"
|
||||||
|
INSERT INTO message_edits (message_id, content_before, content_after, edited_at, edited_by)
|
||||||
|
VALUES (?1, ?2, ?3, ?4, ?5)
|
||||||
|
"#,
|
||||||
|
params![msg_id, old_content, new_content, now, editor_id],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
r#"
|
||||||
|
UPDATE messages
|
||||||
|
SET content = ?1, edited_count = edited_count + 1
|
||||||
|
WHERE id = ?2
|
||||||
|
"#,
|
||||||
|
params![new_content, msg_id],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hard_delete_message(storage_owner: i64, external_user: i64, message_time: i64) -> Result<(), StorageError> {
|
||||||
|
db::with_db(|conn| {
|
||||||
|
let msg_id: i64 = conn.query_row(
|
||||||
|
r#"
|
||||||
|
SELECT id 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, message_time],
|
||||||
|
|row| row.get(0),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
conn.execute("DELETE FROM message_edits WHERE message_id = ?1", params![msg_id])?;
|
||||||
|
conn.execute("DELETE FROM reactions WHERE message_id = ?1", params![msg_id])?;
|
||||||
|
conn.execute("DELETE FROM messages WHERE id = ?1", params![msg_id])?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Marks a message as deleted by the external user rather than removing the row,
|
||||||
|
* so the storage owner still sees a tombstone in the UI.
|
||||||
|
*/
|
||||||
|
pub fn flag_deleted_by_external(storage_owner: i64, external_user: i64, message_time: i64) -> Result<(), StorageError> {
|
||||||
|
db::with_db(|conn| {
|
||||||
|
let affected = conn.execute(
|
||||||
|
r#"
|
||||||
|
UPDATE messages
|
||||||
|
SET deleted_by_external = 1
|
||||||
|
WHERE storage_owner = ?1 AND external_user = ?2 AND message_time = ?3
|
||||||
|
"#,
|
||||||
|
params![storage_owner, external_user, message_time],
|
||||||
|
)?;
|
||||||
|
if affected == 0 {
|
||||||
|
return Err(StorageError::Other("Message not found".into()));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Removes the edit trail but keeps the message with edited_count > 0 so
|
||||||
|
* the UI still shows the "edited" indicator. Only the own user should
|
||||||
|
* call this.
|
||||||
|
*/
|
||||||
|
pub fn delete_edit_history(storage_owner: i64, external_user: i64, message_time: i64) -> Result<(), StorageError> {
|
||||||
|
db::with_db(|conn| {
|
||||||
|
let msg_id: i64 = conn.query_row(
|
||||||
|
r#"
|
||||||
|
SELECT id 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, message_time],
|
||||||
|
|row| row.get(0),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
conn.execute("DELETE FROM message_edits WHERE message_id = ?1", params![msg_id])?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_reaction(
|
||||||
|
storage_owner: i64,
|
||||||
|
external_user: i64,
|
||||||
|
message_time: i64,
|
||||||
|
user_id: i64,
|
||||||
|
reaction: &str,
|
||||||
|
) -> Result<(), StorageError> {
|
||||||
|
db::with_db(|conn| {
|
||||||
|
let msg_id: i64 = conn.query_row(
|
||||||
|
r#"
|
||||||
|
SELECT id 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, message_time],
|
||||||
|
|row| row.get(0),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let now = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_millis() as i64;
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
r#"
|
||||||
|
INSERT OR IGNORE INTO reactions (message_id, user_id, reaction, created_at)
|
||||||
|
VALUES (?1, ?2, ?3, ?4)
|
||||||
|
"#,
|
||||||
|
params![msg_id, user_id, reaction, now],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_reaction(
|
||||||
|
storage_owner: i64,
|
||||||
|
external_user: i64,
|
||||||
|
message_time: i64,
|
||||||
|
user_id: i64,
|
||||||
|
reaction: &str,
|
||||||
|
) -> Result<(), StorageError> {
|
||||||
|
db::with_db(|conn| {
|
||||||
|
let msg_id: i64 = conn.query_row(
|
||||||
|
r#"
|
||||||
|
SELECT id 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, message_time],
|
||||||
|
|row| row.get(0),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"DELETE FROM reactions WHERE message_id = ?1 AND user_id = ?2 AND reaction = ?3",
|
||||||
|
params![msg_id, user_id, reaction],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn add_message(
|
pub fn add_message(
|
||||||
send_time: u128,
|
send_time: u128,
|
||||||
|
|
@ -68,19 +257,12 @@ pub fn add_message(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Insert the message into the DB
|
if let Err(e) = db::with_db(|conn| {
|
||||||
let insert_result = db::with_conn(&MESSAGES_DB, |conn| {
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO messages (
|
INSERT INTO messages (
|
||||||
storage_owner,
|
storage_owner, external_user, message_time, content,
|
||||||
external_user,
|
sent_by_self, message_state, height, reply_to
|
||||||
message_time,
|
|
||||||
content,
|
|
||||||
sent_by_self,
|
|
||||||
message_state,
|
|
||||||
height,
|
|
||||||
reply_to
|
|
||||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
|
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
|
||||||
"#,
|
"#,
|
||||||
params![
|
params![
|
||||||
|
|
@ -99,18 +281,13 @@ pub fn add_message(
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
}) {
|
||||||
|
|
||||||
if let Err(e) = insert_result {
|
|
||||||
log!("Failed to insert message into sqlite: {}", e);
|
log!("Failed to insert message into sqlite: {}", e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update contacts table to reflect that this conversation exists and has a recent message.
|
|
||||||
// Use the Contact helper to set last_message_at to the message timestamp.
|
|
||||||
let mut contact = crate::users::contact::Contact::new(external_user);
|
let mut contact = crate::users::contact::Contact::new(external_user);
|
||||||
contact.set_last_message_at(message_time);
|
contact.set_last_message_at(message_time);
|
||||||
// This will insert or update the contact for the storage owner.
|
|
||||||
crate::util::chats_util::mod_user(storage_owner, &contact);
|
crate::util::chats_util::mod_user(storage_owner, &contact);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,25 +296,21 @@ pub fn change_message_state(
|
||||||
storage_owner: i64,
|
storage_owner: i64,
|
||||||
external_user: i64,
|
external_user: i64,
|
||||||
new_state: MessageState,
|
new_state: MessageState,
|
||||||
) -> io::Result<()> {
|
) -> std::io::Result<()> {
|
||||||
// Run the SELECT and UPDATE inside with_conn to centralize connection access.
|
db::with_db(|conn| {
|
||||||
let res: Result<(), String> = db::with_conn(&MESSAGES_DB, |conn| {
|
|
||||||
let current: Option<String> = match conn.query_row(
|
let current: Option<String> = match conn.query_row(
|
||||||
r#"
|
r#"
|
||||||
SELECT message_state
|
SELECT message_state
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE storage_owner = ?1
|
WHERE storage_owner = ?1 AND external_user = ?2 AND message_time = ?3
|
||||||
AND external_user = ?2
|
ORDER BY id DESC LIMIT 1
|
||||||
AND message_time = ?3
|
|
||||||
ORDER BY id DESC
|
|
||||||
LIMIT 1
|
|
||||||
"#,
|
"#,
|
||||||
params![storage_owner, external_user, timestamp],
|
params![storage_owner, external_user, timestamp],
|
||||||
|row| row.get(0),
|
|row| row.get(0),
|
||||||
) {
|
) {
|
||||||
Ok(state) => Some(state),
|
Ok(state) => Some(state),
|
||||||
Err(rusqlite::Error::QueryReturnedNoRows) => None,
|
Err(rusqlite::Error::QueryReturnedNoRows) => None,
|
||||||
Err(e) => return Err(e),
|
Err(e) => return Err(e.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(current_state_raw) = current else {
|
let Some(current_state_raw) = current else {
|
||||||
|
|
@ -154,24 +327,45 @@ pub fn change_message_state(
|
||||||
UPDATE messages
|
UPDATE messages
|
||||||
SET message_state = ?1
|
SET message_state = ?1
|
||||||
WHERE id = (
|
WHERE id = (
|
||||||
SELECT id
|
SELECT id FROM messages
|
||||||
FROM messages
|
WHERE storage_owner = ?2 AND external_user = ?3 AND message_time = ?4
|
||||||
WHERE storage_owner = ?2
|
ORDER BY id DESC LIMIT 1
|
||||||
AND external_user = ?3
|
|
||||||
AND message_time = ?4
|
|
||||||
ORDER BY id DESC
|
|
||||||
LIMIT 1
|
|
||||||
)
|
)
|
||||||
"#,
|
"#,
|
||||||
params![upgraded, storage_owner, external_user, timestamp],
|
params![upgraded, storage_owner, external_user, timestamp],
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
})
|
||||||
|
.map_err(|e: StorageError| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))
|
||||||
match res {
|
|
||||||
Ok(_) => Ok(()),
|
|
||||||
Err(e) => Err(io::Error::new(io::ErrorKind::Other, e)),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_reactions(conn: &rusqlite::Connection, msg_ids: &[i64]) -> std::collections::HashMap<i64, Vec<String>> {
|
||||||
|
if msg_ids.is_empty() {
|
||||||
|
return std::collections::HashMap::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let placeholders: Vec<String> = msg_ids.iter().enumerate()
|
||||||
|
.map(|(i, _)| format!("?{}", i + 1))
|
||||||
|
.collect();
|
||||||
|
let query = format!(
|
||||||
|
"SELECT message_id, reaction || ':' || COUNT(*) FROM reactions WHERE message_id IN ({}) GROUP BY message_id, reaction",
|
||||||
|
placeholders.join(", ")
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut map: std::collections::HashMap<i64, Vec<String>> = std::collections::HashMap::new();
|
||||||
|
if let Ok(mut stmt) = conn.prepare(&query) {
|
||||||
|
let params: Vec<&dyn rusqlite::types::ToSql> = msg_ids.iter()
|
||||||
|
.map(|id| id as &dyn rusqlite::types::ToSql)
|
||||||
|
.collect();
|
||||||
|
if let Ok(rows) = stmt.query_map(params.as_slice(), |row| {
|
||||||
|
Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?))
|
||||||
|
}) {
|
||||||
|
for row in rows.flatten() {
|
||||||
|
map.entry(row.0).or_default().push(row.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
map
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_messages(
|
pub fn get_messages(
|
||||||
|
|
@ -179,26 +373,17 @@ pub fn get_messages(
|
||||||
external_user: i64,
|
external_user: i64,
|
||||||
loaded_messages: i64,
|
loaded_messages: i64,
|
||||||
amount: i64,
|
amount: i64,
|
||||||
) -> JsonValue {
|
) -> Vec<StoredMessage> {
|
||||||
let messages = array![];
|
|
||||||
|
|
||||||
if amount <= 0 || loaded_messages < 0 {
|
if amount <= 0 || loaded_messages < 0 {
|
||||||
return messages;
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
let res: Result<JsonValue, String> = db::with_conn(&MESSAGES_DB, |conn| {
|
match db::with_db(|conn| {
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT id, message_time, content, sent_by_self, message_state, height, reply_to, edited_count
|
||||||
message_time,
|
|
||||||
content,
|
|
||||||
sent_by_self,
|
|
||||||
message_state,
|
|
||||||
height,
|
|
||||||
reply_to
|
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE storage_owner = ?1
|
WHERE storage_owner = ?1 AND external_user = ?2 AND deleted_by_external = 0
|
||||||
AND external_user = ?2
|
|
||||||
ORDER BY message_time DESC, id DESC
|
ORDER BY message_time DESC, id DESC
|
||||||
LIMIT ?3 OFFSET ?4
|
LIMIT ?3 OFFSET ?4
|
||||||
"#,
|
"#,
|
||||||
|
|
@ -207,49 +392,40 @@ pub fn get_messages(
|
||||||
let rows = stmt.query_map(
|
let rows = stmt.query_map(
|
||||||
params![storage_owner, external_user, amount, loaded_messages],
|
params![storage_owner, external_user, amount, loaded_messages],
|
||||||
|row| {
|
|row| {
|
||||||
let message_time: i64 = row.get(0)?;
|
Ok(StoredMessage {
|
||||||
let content: String = row.get(1)?;
|
id: row.get(0)?,
|
||||||
let sent_by_self: i64 = row.get(2)?;
|
message_time: row.get(1)?,
|
||||||
let message_state: String = row.get(3)?;
|
content: row.get(2)?,
|
||||||
let height: i64 = row.get(4).unwrap_or(0);
|
sent_by_self: row.get::<_, i64>(3)? != 0,
|
||||||
let reply_to: Option<i64> = row.get(5).ok().flatten();
|
message_state: row.get(4)?,
|
||||||
Ok((message_time, content, sent_by_self, message_state, height, reply_to))
|
height: row.get(5).unwrap_or(0),
|
||||||
|
reply_to: row.get(6).ok().flatten(),
|
||||||
|
edited: row.get::<_, i64>(7).unwrap_or(0) > 0,
|
||||||
|
reactions: Vec::new(),
|
||||||
|
})
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let mut out = array![];
|
let mut out = Vec::new();
|
||||||
for row in rows {
|
for row in rows {
|
||||||
match row {
|
match row {
|
||||||
Ok((message_time, content, sent_by_self, message_state, height, reply_to)) => {
|
Ok(msg) => out.push(msg),
|
||||||
let mut msg = object! {
|
Err(e) => log!("Failed to read row from sqlite: {}", e),
|
||||||
"message_time" => message_time,
|
|
||||||
"content" => content,
|
|
||||||
"sent_by_self" => (sent_by_self != 0),
|
|
||||||
"message_state" => message_state,
|
|
||||||
"height" => height
|
|
||||||
};
|
|
||||||
if let Some(rt) = reply_to {
|
|
||||||
let _ = msg.insert("reply_to", rt);
|
|
||||||
}
|
|
||||||
if let Err(e) = out.push(msg) {
|
|
||||||
// out.push returns a JsonError; log it instead of using `?` to avoid
|
|
||||||
// incompatible error conversions inside the DB closure.
|
|
||||||
log!("Failed to append message to output array: {:?}", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
|
||||||
log!("Failed to read row from sqlite: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(out)
|
|
||||||
});
|
|
||||||
|
|
||||||
match res {
|
let msg_ids: Vec<i64> = out.iter().map(|m| m.id).collect();
|
||||||
|
let reaction_map = load_reactions(conn, &msg_ids);
|
||||||
|
for msg in &mut out {
|
||||||
|
msg.reactions = reaction_map.get(&msg.id).cloned().unwrap_or_default();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
|
}) {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log!("Failed to query messages: {}", e);
|
log!("Failed to query messages: {}", e);
|
||||||
messages
|
Vec::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,13 @@
|
||||||
use crate::users::contact::Contact;
|
use crate::users::contact::Contact;
|
||||||
use crate::util::db;
|
use crate::util::db;
|
||||||
use rusqlite::params;
|
use rusqlite::params;
|
||||||
use std::sync::{Arc, LazyLock, Mutex};
|
|
||||||
|
|
||||||
/// Shared DB connection for contacts/messages (created by db helper).
|
|
||||||
static MESSAGES_DB: LazyLock<Arc<Mutex<rusqlite::Connection>>> = LazyLock::new(|| {
|
|
||||||
db::create_general_messages_db().expect("Failed to create or initialize general messages DB")
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Insert or update a contact for the given storage owner.
|
|
||||||
pub fn mod_user(storage_owner: i64, contact: &Contact) {
|
pub fn mod_user(storage_owner: i64, contact: &Contact) {
|
||||||
if let Err(e) = db::with_conn(&MESSAGES_DB, |conn| {
|
if let Err(e) = db::with_db(|conn| {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO contacts (
|
INSERT INTO contacts (storage_owner, user_id, user_name, last_message_at)
|
||||||
storage_owner,
|
VALUES (?1, ?2, ?3, ?4)
|
||||||
user_id,
|
|
||||||
user_name,
|
|
||||||
last_message_at
|
|
||||||
) VALUES (?1, ?2, ?3, ?4)
|
|
||||||
ON CONFLICT(storage_owner, user_id) DO UPDATE SET
|
ON CONFLICT(storage_owner, user_id) DO UPDATE SET
|
||||||
user_name = excluded.user_name,
|
user_name = excluded.user_name,
|
||||||
last_message_at = excluded.last_message_at
|
last_message_at = excluded.last_message_at
|
||||||
|
|
@ -27,7 +16,7 @@ pub fn mod_user(storage_owner: i64, contact: &Contact) {
|
||||||
storage_owner,
|
storage_owner,
|
||||||
contact.user_id,
|
contact.user_id,
|
||||||
contact.user_name.clone(),
|
contact.user_name.clone(),
|
||||||
contact.last_message_at
|
contact.last_message_at,
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -36,9 +25,8 @@ pub fn mod_user(storage_owner: i64, contact: &Contact) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve a single contact for storage_owner/user_id.
|
|
||||||
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 res: Result<Option<Contact>, String> = db::with_conn(&MESSAGES_DB, |conn| {
|
match db::with_db(|conn| {
|
||||||
match conn.query_row(
|
match conn.query_row(
|
||||||
r#"
|
r#"
|
||||||
SELECT user_id, user_name, last_message_at
|
SELECT user_id, user_name, last_message_at
|
||||||
|
|
@ -48,23 +36,18 @@ pub fn get_user(storage_owner: i64, user_id: i64) -> Option<Contact> {
|
||||||
"#,
|
"#,
|
||||||
params![storage_owner, user_id],
|
params![storage_owner, user_id],
|
||||||
|r| {
|
|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 {
|
Ok(Contact {
|
||||||
user_id,
|
user_id: r.get(0)?,
|
||||||
user_name,
|
user_name: r.get(1)?,
|
||||||
last_message_at,
|
last_message_at: r.get(2)?,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Ok(c) => Ok(Some(c)),
|
Ok(c) => Ok(Some(c)),
|
||||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||||
Err(e) => Err(e),
|
Err(e) => Err(e.into()),
|
||||||
}
|
}
|
||||||
});
|
}) {
|
||||||
|
|
||||||
match res {
|
|
||||||
Ok(opt) => opt,
|
Ok(opt) => opt,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error querying user in get_user: {}", e);
|
eprintln!("Error querying user in get_user: {}", e);
|
||||||
|
|
@ -73,11 +56,8 @@ pub fn get_user(storage_owner: i64, user_id: i64) -> Option<Contact> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve all contacts for a storage owner, ordered by last_message_at desc / user_id asc.
|
|
||||||
pub fn get_users(storage_owner: i64) -> Vec<Contact> {
|
pub fn get_users(storage_owner: i64) -> Vec<Contact> {
|
||||||
let contacts_out = Vec::new();
|
match db::with_db(|conn| {
|
||||||
|
|
||||||
let res: Result<Vec<Contact>, String> = db::with_conn(&MESSAGES_DB, |conn| {
|
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
r#"
|
r#"
|
||||||
SELECT user_id, user_name, last_message_at
|
SELECT user_id, user_name, last_message_at
|
||||||
|
|
@ -91,13 +71,10 @@ pub fn get_users(storage_owner: i64) -> Vec<Contact> {
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let rows = stmt.query_map(params![storage_owner], |r| {
|
let rows = 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 {
|
Ok(Contact {
|
||||||
user_id,
|
user_id: r.get(0)?,
|
||||||
user_name,
|
user_name: r.get(1)?,
|
||||||
last_message_at,
|
last_message_at: r.get(2)?,
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|
@ -109,13 +86,11 @@ pub fn get_users(storage_owner: i64) -> Vec<Contact> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(out)
|
Ok(out)
|
||||||
});
|
}) {
|
||||||
|
|
||||||
match res {
|
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to query contacts in get_users: {}", e);
|
eprintln!("Failed to query contacts in get_users: {}", e);
|
||||||
contacts_out
|
Vec::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,22 @@
|
||||||
use crate::util::db;
|
use crate::util::db;
|
||||||
use json::Array;
|
|
||||||
use rusqlite::params;
|
use rusqlite::params;
|
||||||
use std::sync::{Arc, LazyLock, Mutex};
|
|
||||||
|
|
||||||
static MESSAGES_DB: LazyLock<Arc<Mutex<rusqlite::Connection>>> = LazyLock::new(|| {
|
#[derive(Debug, Clone)]
|
||||||
db::create_general_messages_db().expect("Failed to create or initialize general messages DB")
|
pub struct StoredCommunity {
|
||||||
});
|
pub address: String,
|
||||||
|
pub title: String,
|
||||||
|
pub position: String,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct CommunitiesUtil;
|
pub struct CommunitiesUtil;
|
||||||
|
|
||||||
impl CommunitiesUtil {
|
impl CommunitiesUtil {
|
||||||
pub fn add_community(storage_owner: i64, address: String, title: String, position: String) {
|
pub fn add_community(storage_owner: i64, address: String, title: String, position: String) {
|
||||||
if let Err(e) = db::with_conn(&MESSAGES_DB, |conn| {
|
if let Err(e) = db::with_db(|conn| {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO communities (
|
INSERT INTO communities (storage_owner, address, title, position)
|
||||||
storage_owner,
|
VALUES (?1, ?2, ?3, ?4)
|
||||||
address,
|
|
||||||
title,
|
|
||||||
position
|
|
||||||
) VALUES (?1, ?2, ?3, ?4)
|
|
||||||
ON CONFLICT(storage_owner, address) DO UPDATE SET
|
ON CONFLICT(storage_owner, address) DO UPDATE SET
|
||||||
title = excluded.title,
|
title = excluded.title,
|
||||||
position = excluded.position
|
position = excluded.position
|
||||||
|
|
@ -33,7 +30,7 @@ impl CommunitiesUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_community(storage_owner: i64, community_address: String) {
|
pub fn remove_community(storage_owner: i64, community_address: String) {
|
||||||
if let Err(e) = db::with_conn(&MESSAGES_DB, |conn| {
|
if let Err(e) = db::with_db(|conn| {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"DELETE FROM communities WHERE storage_owner = ?1 AND address = ?2",
|
"DELETE FROM communities WHERE storage_owner = ?1 AND address = ?2",
|
||||||
params![storage_owner, community_address],
|
params![storage_owner, community_address],
|
||||||
|
|
@ -44,10 +41,8 @@ impl CommunitiesUtil {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_communities(storage_owner: i64) -> Array {
|
pub fn get_communities(storage_owner: i64) -> Vec<StoredCommunity> {
|
||||||
let communities_out = Array::new();
|
match db::with_db(|conn| {
|
||||||
|
|
||||||
let res: Result<Array, String> = db::with_conn(&MESSAGES_DB, |conn| {
|
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
r#"
|
r#"
|
||||||
SELECT address, title, position
|
SELECT address, title, position
|
||||||
|
|
@ -57,33 +52,26 @@ impl CommunitiesUtil {
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let rows = stmt.query_map(params![storage_owner], |r| {
|
let rows = stmt.query_map(params![storage_owner], |r| {
|
||||||
let address: String = r.get(0)?;
|
Ok(StoredCommunity {
|
||||||
let title: String = r.get(1)?;
|
address: r.get(0)?,
|
||||||
let position: String = r.get(2)?;
|
title: r.get(1)?,
|
||||||
Ok((address, title, position))
|
position: r.get(2)?,
|
||||||
|
})
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut out = Array::new();
|
let mut out = Vec::new();
|
||||||
for row in rows {
|
for row in rows {
|
||||||
match row {
|
match row {
|
||||||
Ok((address, title, position)) => {
|
Ok(community) => out.push(community),
|
||||||
let mut community = json::JsonValue::new_object();
|
|
||||||
community["title"] = json::JsonValue::String(title);
|
|
||||||
community["address"] = json::JsonValue::String(address);
|
|
||||||
community["position"] = json::JsonValue::String(position);
|
|
||||||
out.push(community);
|
|
||||||
}
|
|
||||||
Err(e) => eprintln!("Failed to read community row: {}", e),
|
Err(e) => eprintln!("Failed to read community row: {}", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(out)
|
Ok(out)
|
||||||
});
|
}) {
|
||||||
|
Ok(v) => v,
|
||||||
match res {
|
|
||||||
Ok(arr) => arr,
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to query communities in get_communities: {}", e);
|
eprintln!("Failed to query communities in get_communities: {}", e);
|
||||||
communities_out
|
Vec::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,17 +51,17 @@ impl Default for IotaConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_config() {
|
pub fn load_config() {
|
||||||
let s = load_file("", "config.json");
|
let s = load_file("", "config.yaml");
|
||||||
if s.is_empty() {
|
if s.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
match serde_json::from_str::<IotaConfig>(&s) {
|
match serde_yaml::from_str::<IotaConfig>(&s) {
|
||||||
Ok(parsed) => {
|
Ok(parsed) => {
|
||||||
CONFIG.store(Arc::new(parsed));
|
CONFIG.store(Arc::new(parsed));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to parse config.json: {}. Content: '{}'", e, s);
|
eprintln!("Failed to parse config.yaml: {}. Content: '{}'", e, s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -72,8 +72,8 @@ pub fn clear_config() {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save_config() {
|
pub fn save_config() {
|
||||||
if let Ok(json) = serde_json::to_string(&**CONFIG.load()) {
|
if let Ok(yaml) = serde_yaml::to_string(&**CONFIG.load()) {
|
||||||
save_file("", "config.json", &json);
|
save_file("", "config.yaml", &yaml);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,88 +1,207 @@
|
||||||
//! Database helper utilities.
|
|
||||||
//!
|
|
||||||
//! This module provides small helpers to open/init sqlite databases and to
|
|
||||||
//! create a shared (Arc<Mutex<Connection>>) connection wrapper callers can
|
|
||||||
//! reuse. The goal is to centralize the "open and initialize" logic and
|
|
||||||
//! provide small convenience helpers used by other util modules.
|
|
||||||
|
|
||||||
use iota_util::file_util::get_directory;
|
use iota_util::file_util::get_directory;
|
||||||
use rusqlite::{Connection, Error as RusqliteError};
|
use once_cell::sync::Lazy;
|
||||||
|
use r2d2::ManageConnection;
|
||||||
|
use rusqlite::Connection;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
/// Returns the file path for a named DB inside the application's data directory.
|
use crate::storage_error::StorageError;
|
||||||
///
|
|
||||||
/// Arguments:
|
const DB_NAME: &str = "messages";
|
||||||
/// - `db_name` : name of the DB (without extension). Example: `"messages"`.
|
|
||||||
pub fn db_file_path(db_name: &str) -> String {
|
/// A simple r2d2 manager for rusqlite connections.
|
||||||
|
pub struct SqliteManager;
|
||||||
|
|
||||||
|
impl ManageConnection for SqliteManager {
|
||||||
|
type Connection = Connection;
|
||||||
|
type Error = rusqlite::Error;
|
||||||
|
|
||||||
|
fn connect(&self) -> Result<Connection, rusqlite::Error> {
|
||||||
|
let path = db_file_path(DB_NAME);
|
||||||
|
let conn = Connection::open(path)?;
|
||||||
|
conn.execute_batch("PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL;")?;
|
||||||
|
conn.busy_timeout(Duration::from_millis(250))?;
|
||||||
|
Ok(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_valid(&self, conn: &mut Connection) -> Result<(), rusqlite::Error> {
|
||||||
|
conn.execute_batch("SELECT 1")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_broken(&self, _conn: &mut Connection) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static POOL: Lazy<Arc<r2d2::Pool<SqliteManager>>> = Lazy::new(|| {
|
||||||
|
let manager = SqliteManager;
|
||||||
|
let pool = r2d2::Pool::builder()
|
||||||
|
.max_size(8)
|
||||||
|
.build(manager)
|
||||||
|
.expect("Failed to create database connection pool");
|
||||||
|
run_migrations(&pool).expect("Failed to run database migrations");
|
||||||
|
Arc::new(pool)
|
||||||
|
});
|
||||||
|
|
||||||
|
pub fn pool() -> Arc<r2d2::Pool<SqliteManager>> {
|
||||||
|
POOL.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_db<T, F>(f: F) -> Result<T, StorageError>
|
||||||
|
where
|
||||||
|
F: FnOnce(&Connection) -> Result<T, StorageError>,
|
||||||
|
{
|
||||||
|
let conn = POOL.get().map_err(|e| StorageError::Pool(e.to_string()))?;
|
||||||
|
f(&conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn db_file_path(db_name: &str) -> String {
|
||||||
let mut p = PathBuf::from(get_directory());
|
let mut p = PathBuf::from(get_directory());
|
||||||
p.push(format!("{db_name}.sqlite3"));
|
p.push(format!("{db_name}.sqlite3"));
|
||||||
p.to_string_lossy().to_string()
|
p.to_string_lossy().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open a sqlite connection to the named DB file (no initialization).
|
fn run_migrations(pool: &r2d2::Pool<SqliteManager>) -> Result<(), StorageError> {
|
||||||
///
|
let conn = pool.get().map_err(|e| StorageError::Pool(e.to_string()))?;
|
||||||
/// Arguments:
|
let current_version: i64 = conn
|
||||||
/// - `db_name`: name of the DB (without extension).
|
.pragma_query_value(None, "user_version", |r| r.get(0))
|
||||||
pub fn open_connection(db_name: &str) -> Result<Connection, RusqliteError> {
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
if current_version < 1 {
|
||||||
|
conn.execute_batch(
|
||||||
|
r#"
|
||||||
|
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,
|
||||||
|
height INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_messages_lookup
|
||||||
|
ON messages (storage_owner, external_user, message_time DESC);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS communities (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
storage_owner INTEGER NOT NULL,
|
||||||
|
address TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
position TEXT NOT NULL,
|
||||||
|
UNIQUE(storage_owner, address)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_communities_owner
|
||||||
|
ON communities (storage_owner);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
user_id INTEGER PRIMARY KEY,
|
||||||
|
username TEXT NOT NULL UNIQUE,
|
||||||
|
public_key TEXT NOT NULL,
|
||||||
|
private_key_hash TEXT NOT NULL,
|
||||||
|
reset_token TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
display_name TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS trusted_apps (
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
app_id TEXT NOT NULL,
|
||||||
|
app_secret TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (user_id, app_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
PRAGMA user_version = 1;
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if current_version < 2 {
|
||||||
|
conn.execute_batch(
|
||||||
|
r#"
|
||||||
|
ALTER TABLE messages ADD COLUMN height INTEGER NOT NULL DEFAULT 0;
|
||||||
|
PRAGMA user_version = 2;
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if current_version < 3 {
|
||||||
|
conn.execute_batch(
|
||||||
|
r#"
|
||||||
|
ALTER TABLE messages ADD COLUMN reply_to INTEGER;
|
||||||
|
PRAGMA user_version = 3;
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if current_version < 4 {
|
||||||
|
conn.execute_batch(
|
||||||
|
r#"
|
||||||
|
ALTER TABLE messages ADD COLUMN edited_count INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE messages ADD COLUMN deleted_by_external INTEGER NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS message_edits (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
message_id INTEGER NOT NULL REFERENCES messages(id),
|
||||||
|
content_before TEXT NOT NULL,
|
||||||
|
content_after TEXT NOT NULL,
|
||||||
|
edited_at INTEGER NOT NULL,
|
||||||
|
edited_by INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_message_edits_msg
|
||||||
|
ON message_edits (message_id, edited_at DESC);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS reactions (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
message_id INTEGER NOT NULL REFERENCES messages(id),
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
reaction TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
UNIQUE(message_id, user_id, reaction)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_reactions_msg
|
||||||
|
ON reactions (message_id, reaction);
|
||||||
|
|
||||||
|
PRAGMA user_version = 4;
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_connection(db_name: &str) -> Result<Connection, rusqlite::Error> {
|
||||||
let path = db_file_path(db_name);
|
let path = db_file_path(db_name);
|
||||||
Connection::open(path)
|
Connection::open(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open a connection and immediately run `init_sql` via `execute_batch`.
|
|
||||||
///
|
|
||||||
/// Arguments:
|
|
||||||
/// - `db_name`: name of the DB (without extension).
|
|
||||||
/// - `init_sql`: SQL statements to initialize schema & PRAGMAs (can be multiple).
|
|
||||||
pub fn open_and_init(db_name: &str, init_sql: &str) -> Result<Connection, RusqliteError> {
|
|
||||||
let conn = open_connection(db_name)?;
|
|
||||||
conn.execute_batch(init_sql)?;
|
|
||||||
Ok(conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a shared, Arc<Mutex<Connection>> initialized with the given SQL.
|
|
||||||
///
|
|
||||||
/// This is a convenience wrapper that returns an owned Arc<Mutex<Connection>>
|
|
||||||
/// so caller modules can store it in a `static` or pass it around.
|
|
||||||
///
|
|
||||||
/// Arguments:
|
|
||||||
/// - `db_name`: DB name (without extension).
|
|
||||||
/// - `init_sql`: init SQL (eg PRAGMA + CREATE TABLE statements).
|
|
||||||
pub fn create_shared_connection(
|
pub fn create_shared_connection(
|
||||||
db_name: &str,
|
db_name: &str,
|
||||||
init_sql: &str,
|
init_sql: &str,
|
||||||
) -> Result<Arc<Mutex<Connection>>, String> {
|
) -> Result<Arc<std::sync::Mutex<Connection>>, String> {
|
||||||
match open_and_init(db_name, init_sql) {
|
let path = db_file_path(db_name);
|
||||||
Ok(conn) => {
|
let conn = Connection::open(path).map_err(|e| e.to_string())?;
|
||||||
// Configure some sensible defaults for concurrency
|
conn.execute_batch(init_sql).map_err(|e| e.to_string())?;
|
||||||
// Attempt to set a busy timeout to reduce SQLITE_BUSY failures.
|
|
||||||
let _ = conn.busy_timeout(Duration::from_millis(250));
|
let _ = conn.busy_timeout(Duration::from_millis(250));
|
||||||
Ok(Arc::new(Mutex::new(conn)))
|
Ok(Arc::new(std::sync::Mutex::new(conn)))
|
||||||
}
|
|
||||||
Err(e) => Err(format!("Failed to open/init DB '{}': {}", db_name, e)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Acquire the Connection from an Arc<Mutex<Connection>> and run the provided
|
pub fn with_conn<T, F>(shared: &Arc<std::sync::Mutex<Connection>>, f: F) -> Result<T, String>
|
||||||
/// closure. Converts rusqlite::Error into a String on error.
|
|
||||||
///
|
|
||||||
/// Arguments:
|
|
||||||
/// - `shared`: Arc<Mutex<Connection>>
|
|
||||||
/// - `f`: closure that receives &Connection and returns Result<T, RusqliteError>
|
|
||||||
///
|
|
||||||
/// Returns Ok(T) or Err(String).
|
|
||||||
pub fn with_conn<T, F>(shared: &Arc<Mutex<Connection>>, f: F) -> Result<T, String>
|
|
||||||
where
|
where
|
||||||
F: FnOnce(&Connection) -> Result<T, RusqliteError>,
|
F: FnOnce(&Connection) -> Result<T, rusqlite::Error>,
|
||||||
{
|
{
|
||||||
// When invoked from within an async runtime (such as Tokio), taking a blocking
|
|
||||||
// std::sync::Mutex lock on the runtime thread can cause deadlocks or permanent
|
|
||||||
// awaits. Detect whether we're running inside a Tokio runtime and, if so,
|
|
||||||
// execute the blocking lock + database closure using Tokio's blocking helper.
|
|
||||||
//
|
|
||||||
// The blocking section returns Result<T, String> so we can propagate errors
|
|
||||||
// in the same form as before.
|
|
||||||
if tokio::runtime::Handle::try_current().is_ok() {
|
if tokio::runtime::Handle::try_current().is_ok() {
|
||||||
tokio::task::block_in_place(|| {
|
tokio::task::block_in_place(|| {
|
||||||
let guard = shared
|
let guard = shared
|
||||||
|
|
@ -98,97 +217,7 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize a general-purpose messages+contacts DB and return a shared
|
/// Legacy - kept for e2ee_storage which uses its own DB.
|
||||||
/// connection. This helper creates a single DB file that can contain multiple
|
pub fn create_general_messages_db() -> Result<Arc<std::sync::Mutex<Connection>>, String> {
|
||||||
/// tables (messages, contacts, ...). The SQL here is conservative and intended
|
create_shared_connection(DB_NAME, "")
|
||||||
/// to be safe if called multiple times.
|
|
||||||
///
|
|
||||||
/// Callers may prefer to call `create_shared_connection("messages", INIT_SQL)`
|
|
||||||
/// directly, but this convenience is useful for code that expects both tables.
|
|
||||||
pub fn create_general_messages_db() -> Result<Arc<Mutex<Connection>>, String> {
|
|
||||||
// Keep PRAGMA and schema in one multi-statement string so callers only
|
|
||||||
// need to call a single execute_batch.
|
|
||||||
const INIT_SQL: &str = 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,
|
|
||||||
height INTEGER NOT NULL DEFAULT 0
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_messages_lookup
|
|
||||||
ON messages (storage_owner, external_user, message_time DESC);
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS communities (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
storage_owner INTEGER NOT NULL,
|
|
||||||
address TEXT NOT NULL,
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
position TEXT NOT NULL,
|
|
||||||
UNIQUE(storage_owner, address)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_communities_owner
|
|
||||||
ON communities (storage_owner);
|
|
||||||
"#;
|
|
||||||
|
|
||||||
match create_shared_connection("messages", INIT_SQL) {
|
|
||||||
Ok(shared_conn) => {
|
|
||||||
// Attempt to add the height column for backwards compatibility.
|
|
||||||
// This will fail if the column already exists, which is expected.
|
|
||||||
let _ = with_conn(&shared_conn, |conn| {
|
|
||||||
let _ = conn.execute(
|
|
||||||
"ALTER TABLE messages ADD COLUMN height INTEGER NOT NULL DEFAULT 0",
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
// Attempt to add the reply_to column for backwards compatibility.
|
|
||||||
// This will fail if the column already exists, which is expected.
|
|
||||||
let _ = with_conn(&shared_conn, |conn| {
|
|
||||||
let _ = conn.execute(
|
|
||||||
"ALTER TABLE messages ADD COLUMN reply_to INTEGER",
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
Ok(shared_conn)
|
|
||||||
}
|
}
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Example usage:
|
|
||||||
|
|
||||||
// In some util module (at init time, e.g. lazy_static or LazyLock)
|
|
||||||
static MESSAGES_DB: LazyLock<Arc<Mutex<Connection>>> = LazyLock::new(|| {
|
|
||||||
create_general_messages_db().expect("failed to create messages DB")
|
|
||||||
});
|
|
||||||
|
|
||||||
// Later, to run a query:
|
|
||||||
let res: Result<Vec<MyRow>, String> = with_conn(&MESSAGES_DB, |conn| {
|
|
||||||
let mut stmt = conn.prepare("SELECT ...")?;
|
|
||||||
let rows = stmt.query_map(...)?;
|
|
||||||
// collect and return Ok(...)
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -2,6 +2,7 @@ use crate::omikron_connection::OmikronConnection;
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use iota_state::APP_STATE;
|
use iota_state::APP_STATE;
|
||||||
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use tokio::time::Duration;
|
use tokio::time::Duration;
|
||||||
|
|
@ -16,6 +17,8 @@ impl OmikronConnection {
|
||||||
|
|
||||||
PING_TIMES.retain(|_, v| v.elapsed() < Duration::from_secs(30));
|
PING_TIMES.retain(|_, v| v.elapsed() < Duration::from_secs(30));
|
||||||
|
|
||||||
|
self.missed_pongs.fetch_add(1, Ordering::Relaxed);
|
||||||
|
|
||||||
let ping_message = CommunicationValue::new(CommunicationType::Ping)
|
let ping_message = CommunicationValue::new(CommunicationType::Ping)
|
||||||
.with_id(id)
|
.with_id(id)
|
||||||
.add_typed_default(
|
.add_typed_default(
|
||||||
|
|
@ -23,10 +26,12 @@ impl OmikronConnection {
|
||||||
DataValue::Array(vec![DataValue::SignedNumber(*self.last_ping.lock().await as i128)]),
|
DataValue::Array(vec![DataValue::SignedNumber(*self.last_ping.lock().await as i128)]),
|
||||||
);
|
);
|
||||||
|
|
||||||
self.send_message(&ping_message).await;
|
let _ = self.send_message(&ping_message).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_pong(&self, cv: &CommunicationValue) {
|
pub async fn handle_pong(&self, cv: &CommunicationValue) {
|
||||||
|
self.missed_pongs.store(0, Ordering::Relaxed);
|
||||||
|
|
||||||
let id = cv.get_id();
|
let id = cv.get_id();
|
||||||
|
|
||||||
if let Some((_, send_time)) = PING_TIMES.remove(&id) {
|
if let Some((_, send_time)) = PING_TIMES.remove(&id) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue