diff --git a/Cargo.lock b/Cargo.lock index efc236d..d743298 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,6 +6,7 @@ version = 4 name = "Iota" version = "0.1.0" dependencies = [ + "aes-gcm", "axum", "base64", "bytes", @@ -13,8 +14,11 @@ dependencies = [ "color-eyre", "crossterm 0.29.0", "der", + "futures", "futures-util", "hex", + "hkdf", + "hmac", "json", "once_cell", "pkcs8", @@ -29,6 +33,7 @@ dependencies = [ "sysinfo", "tokio", "tokio-tungstenite", + "tungstenite", "uuid", "walkdir", "x448", @@ -50,6 +55,41 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -235,6 +275,16 @@ dependencies = [ "windows-link 0.2.0", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "cmake" version = "0.1.54" @@ -409,9 +459,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "darling" version = "0.20.11" @@ -501,6 +561,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -751,6 +812,16 @@ dependencies = [ "wasi 0.14.4+wasi-0.2.4", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gimli" version = "0.31.1" @@ -799,6 +870,24 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "http" version = "1.3.1" @@ -1085,6 +1174,15 @@ version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "instability" version = "0.3.9" @@ -1321,6 +1419,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl" version = "0.10.73" @@ -1434,6 +1538,18 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "potential_utf" version = "0.1.3" @@ -2332,6 +2448,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index 56095f5..af7c669 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,3 +35,8 @@ x509 = "*" sha2 = "*" der = "*" pkcs8 = { version = "*", features = ["alloc"] } +tungstenite = "*" +futures = "*" +aes-gcm = "*" +hkdf = "*" +hmac = "*" diff --git a/src/auth/crypto_helper.rs b/src/auth/crypto_helper.rs new file mode 100644 index 0000000..5d4f847 --- /dev/null +++ b/src/auth/crypto_helper.rs @@ -0,0 +1,138 @@ +use aes_gcm::{ + Aes256Gcm, // AES‑256 GCM + Nonce, + aead::{Aead, KeyInit, OsRng}, +}; +use base64::{Engine as _, engine::general_purpose::STANDARD}; +use base64::{decode as b64_decode, encode as b64_encode}; +use hex; +use json::JsonValue; +use once_cell::sync::Lazy; +use rand::Rng; +use rand_core::RngCore; +use sha2::{Digest, Sha256}; +use std::io; +use std::sync::Mutex; +use uuid::Uuid; +use x448::{PublicKey, Secret, SharedSecret}; // from the `x448` crate + +/// Errors for crypto operations +#[derive(Debug)] +pub enum CryptoError { + Base64Decode(base64::DecodeError), + InvalidKey, + AgreementError, + EncryptionError(aes_gcm::Error), + DecryptionError(aes_gcm::Error), +} + +impl From for CryptoError { + fn from(err: base64::DecodeError) -> Self { + CryptoError::Base64Decode(err) + } +} + +pub struct KeyPair { + pub secret: Secret, + pub public: PublicKey, +} + +pub fn generate_keypair() -> KeyPair { + let mut buf = [0u8; 56]; + let mut rng = OsRng; + rng.fill_bytes(&mut buf); + let secret = Secret::from_bytes(&buf).unwrap(); + let public = PublicKey::from(&secret); + KeyPair { secret, public } +} + +pub fn public_key_to_base64(pubkey: &PublicKey) -> String { + b64_encode(pubkey.as_bytes().as_ref()) +} + +pub fn secret_key_to_base64(secret: &Secret) -> String { + b64_encode(secret.as_bytes().as_ref()) +} + +pub fn load_public_key(base64_pub: &str) -> Option { + let bytes = b64_decode(base64_pub).unwrap(); + PublicKey::from_bytes(&bytes) +} + +pub fn load_secret_key(base64_secret: &str) -> Option { + let bytes = b64_decode(base64_secret).unwrap(); + Secret::from_bytes(&bytes) +} + +fn derive_aes_key(shared: &SharedSecret) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(shared.as_bytes()); + let result = hasher.finalize(); + let mut key = [0u8; 32]; + key.copy_from_slice(&result[..32]); + key +} + +pub fn encrypt( + base64_secret: &str, + base64_peer_pub: &str, + plaintext: &str, +) -> Result { + let secret = load_secret_key(base64_secret).unwrap(); + let peer_pub = load_public_key(base64_peer_pub).unwrap(); + let shared = secret + .to_diffie_hellman(&peer_pub) + .ok_or(CryptoError::AgreementError)?; + let key_bytes = derive_aes_key(&shared); + let cipher = Aes256Gcm::new_from_slice(&key_bytes).expect("Key length should be correct"); + let mut nonce_bytes = [0u8; 12]; + OsRng.fill_bytes(&mut nonce_bytes); + let nonce = Nonce::from_slice(&nonce_bytes); + let ciphertext = cipher + .encrypt(nonce, plaintext.as_bytes()) + .map_err(CryptoError::EncryptionError)?; + // prefix nonce to ciphertext + let mut out = Vec::with_capacity(nonce_bytes.len() + ciphertext.len()); + out.extend_from_slice(&nonce_bytes); + out.extend_from_slice(&ciphertext); + Ok(b64_encode(&out)) +} + +pub fn decrypt( + base64_secret: &str, + base64_peer_pub: &str, + encrypted_base64: &str, +) -> Result { + let secret = load_secret_key(base64_secret).unwrap(); + let peer_pub = load_public_key(base64_peer_pub).unwrap(); + let shared = secret + .to_diffie_hellman(&peer_pub) + .ok_or(CryptoError::AgreementError)?; + let key_bytes = derive_aes_key(&shared); + let cipher = Aes256Gcm::new_from_slice(&key_bytes).expect("Key length should be correct"); + + let encrypted = b64_decode(encrypted_base64)?; + if encrypted.len() < 12 { + return Err(CryptoError::DecryptionError(aes_gcm::Error)); + } + let nonce_bytes = &encrypted[..12]; + let ciphertext = &encrypted[12..]; + let nonce = Nonce::from_slice(nonce_bytes); + let plaintext_bytes = cipher + .decrypt(nonce, ciphertext) + .map_err(CryptoError::DecryptionError)?; + let plaintext = String::from_utf8(plaintext_bytes) + .map_err(|_| CryptoError::DecryptionError(aes_gcm::Error))?; + Ok(plaintext) +} + +pub fn hash_it(input: &str) -> Vec { + let mut hasher = Sha256::new(); + hasher.update(input.as_bytes()); + hasher.finalize().to_vec() +} + +pub fn hex_hash(input: &str) -> String { + let digest = hash_it(input); + digest.iter().map(|b| format!("{:02x}", b)).collect() +} diff --git a/src/auth/mod.rs b/src/auth/mod.rs index f442b83..8eef5c3 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -1 +1,2 @@ -pub mod auth_connector; \ No newline at end of file +pub mod auth_connector; +pub mod crypto_helper; diff --git a/src/communities/community.rs b/src/communities/community.rs new file mode 100644 index 0000000..26b2337 --- /dev/null +++ b/src/communities/community.rs @@ -0,0 +1,280 @@ +use crate::communities::interactables::category::Category; +use crate::communities::interactables::registry; +use crate::communities::interactables::text_chat::TextChat; +use crate::communities::interactables::voice_chat::VoiceChat; +use crate::communities::{ + community_connection::CommunityConnection, interactables::interactable::Interactable, +}; +use crate::data::communication::{CommunicationType, CommunicationValue}; +use crate::util::file_util; +use base64::{Engine as _, engine::general_purpose::STANDARD}; +use json::JsonValue; +use json::object::Object; +use rand::RngCore; +use rand_core::OsRng; +use ratatui::text; +use serde::de::value::StringDeserializer; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; +use uuid::Uuid; +use x448::{PublicKey, Secret}; +/// Permissions +// uuid -> interactable/path/like/this/interactable_name:permission +// -> role + +/// Roles +// rolename -> interactable/path/like/this/interactable_name:permission +// -> other/path/like/this/interactable_name:permission + +pub struct Community { + name: String, + owner_id: Uuid, + members: Vec, + permissions: HashMap>, + roles: HashMap>, + private_key: Secret, + public_key: PublicKey, + pub interactables: Arc>>>>, + pub connections: Arc>>>>, +} + +impl Community { + pub fn new() -> Self { + let mut buf = [0u8; 56]; + let mut rng = OsRng; + rng.fill_bytes(&mut buf); + let private_key = Secret::from_bytes(&buf).unwrap(); + let public_key = PublicKey::from(&private_key); + Community { + name: String::new(), + owner_id: Uuid::new_v4(), + members: Vec::new(), + permissions: HashMap::new(), + roles: HashMap::new(), + private_key, + public_key, + interactables: Arc::new(RwLock::new(Vec::new())), + connections: Arc::new(RwLock::new(HashMap::new())), + } + } + + pub fn add_member(&mut self, member_id: Uuid) { + self.members.push(member_id); + } + + pub fn remove_member(&mut self, member_id: Uuid) { + self.members.retain(|id| *id != member_id); + } + pub fn get_name(&self) -> &str { + &self.name + } + pub fn get_private_key(&self) -> Secret { + Secret::from_bytes(self.private_key.as_bytes()).unwrap() + } + pub fn get_public_key(&self) -> &PublicKey { + &self.public_key + } + pub async fn add_connection(self: &Arc, other: Arc) { + let mut vec = self + .connections + .read() + .await + .get(&other.get_user_id().await.unwrap()) + .cloned() + .unwrap_or_default(); + vec.push(other.clone()); + self.connections + .write() + .await + .insert(other.get_user_id().await.unwrap(), vec); + } + pub async fn get_connections(&self) -> HashMap>> { + self.connections.read().await.clone() + } + pub async fn get_connections_for_user(&self, user_id: Uuid) -> Vec> { + self.connections + .read() + .await + .get(&user_id) + .cloned() + .unwrap_or_default() + } + pub async fn get_interactables( + &self, + user_id: Uuid, + ) -> Vec>> { + self.interactables.read().await.clone() + } + pub async fn add_interactable(self: &mut Arc, interactable: Arc>) { + self.interactables.write().await.push(interactable); + } + pub async fn remove_interactable( + self: &mut Arc, + interactable: Arc>, + ) { + self.interactables + .write() + .await + .retain(|i| !Arc::ptr_eq(i, &interactable)); + } + pub async fn run_function( + self: &mut Arc, + user_id: Uuid, + name: &str, + path: &str, + function: &str, + cv: &CommunicationValue, + ) -> CommunicationValue { + if path.is_empty() { + let mut target_interactables = &self.interactables.read().await.clone(); + for interactable in target_interactables.iter() { + if interactable.get_name() == name { + if interactable.get_codec() == "category" { + return CommunicationValue::new(CommunicationType::error); + } else { + return interactable.run_function(cv.clone()); + } + } + } + } else { + let target_interactables = &self.interactables.read().await.clone(); + for interactable in target_interactables.iter() { + if interactable.get_name() == name { + if interactable.get_codec() == "category" { + let category: &Category = + interactable.as_any().downcast_ref::().unwrap(); + return category + .get_child(path.to_string(), name.to_string()) + .unwrap() + .run_function(cv.clone()); + } else { + return CommunicationValue::new(CommunicationType::error); + } + } + } + } + CommunicationValue::new(CommunicationType::add_chat) + } + + pub async fn save(&self) { + let mut json = Object::new(); + json.insert("name", JsonValue::String(self.name.clone())); + json.insert("owner_id", JsonValue::String(self.owner_id.to_string())); + + json.insert( + "private_key", + JsonValue::String(STANDARD.encode(&self.private_key.as_bytes())), + ); + json.insert( + "public_key", + JsonValue::String(STANDARD.encode(&self.public_key.as_bytes())), + ); + + file_util::save_file( + &format!("communities/{}/", self.name), + "config.json", + &json.dump(), + ); + + let mut user_data = Object::new(); + for user in self.members.iter() { + let mut data = JsonValue::new_object(); + + let mut permissions = JsonValue::new_array(); + for perm in self.permissions.get(user).unwrap() { + permissions.push(perm.to_string()); + } + + data.insert("permissions", permissions); + user_data.insert(&user.to_string(), data); + } + file_util::save_file( + &format!("communities/{}/", self.name), + "users.json", + &user_data.dump(), + ); + + for interactable in self.interactables.read().await.clone().iter() { + registry::save(interactable).await; + } + } +} +pub async fn load(name: &String) -> Option> { + let file_contents = file_util::load_file(&format!("communities/{}/", name), "config.json"); + let json_content = json::parse(&file_contents).unwrap(); + + let user_data = file_util::load_file(&format!("communities/{}/", name), "users.json"); + let user_json: JsonValue = json::parse(&user_data).unwrap(); + let mut users = Vec::new(); + let mut permissions: HashMap> = HashMap::new(); + + for user in user_json.entries() { + let (str, json): (&str, &JsonValue) = user; + let perms_j = &json["permissions"]; + let perms = Vec::new(); + for i in perms_j.entries() { + // let perm_j = i.as_str().unwrap(); + // perms.push(perm_j.to_string()); + } + + let user_id = Uuid::parse_str(str).unwrap(); + + users.push(user_id); + permissions.insert(user_id, perms); + } + + let role_data = file_util::load_file(&format!("communities/{}/", name), "roles.json"); + if let Ok(user_json) = json::parse(&role_data) { + } else { + return None; + }; + let mut roles: HashMap> = HashMap::new(); + + let community = Community { + name: json_content["name"].as_str().unwrap().to_string(), + owner_id: Uuid::parse_str(json_content["owner_id"].as_str().unwrap()).unwrap(), + members: users, + roles, + permissions, + private_key: Secret::from_bytes( + &STANDARD + .decode(json_content["private_key"].as_str().unwrap()) + .unwrap(), + ) + .unwrap(), + public_key: PublicKey::from( + &Secret::from_bytes( + &STANDARD + .decode(json_content["private_key"].as_str().unwrap()) + .unwrap(), + ) + .unwrap(), + ), + interactables: Arc::new(RwLock::new(Vec::new())), + connections: Arc::new(RwLock::new(HashMap::new())), + }; + let mut comarc = Arc::new(community); + + let interactable_files: Vec = + file_util::get_children(&format!("communities/{}/interactables/", name)); + for file in interactable_files { + if file.contains(".json") { + let name = file.split('.').next().unwrap().to_string(); + let interactable: Box = + registry::load(comarc.clone(), String::new(), name).await; + comarc.add_interactable(Arc::new(interactable)).await; + } + } + let mut text_chat: TextChat = TextChat::new(); + text_chat.load( + comarc.clone(), + String::new(), + String::from("a"), + &JsonValue::Null, + ); + + comarc.add_interactable(Arc::new(Box::new(text_chat))).await; + + Some(comarc) +} diff --git a/src/communities/community_connection.rs b/src/communities/community_connection.rs new file mode 100644 index 0000000..970ab85 --- /dev/null +++ b/src/communities/community_connection.rs @@ -0,0 +1,415 @@ +use crate::auth::auth_connector::AuthUser; +use crate::auth::auth_connector::get_user; +use crate::communities::community::Community; +use crate::communities::interactables::interactable::Interactable; +use crate::data::communication::{CommunicationType, CommunicationValue, DataTypes}; +use aes_gcm::{Aes256Gcm, KeyInit, Nonce, aead::Aead}; +use base64::{Engine as _, engine::general_purpose::STANDARD}; +use futures::SinkExt; +use hkdf::Hkdf; +use json::JsonValue; +use json::object::Object; +use rand::{Rng, distributions::Alphanumeric}; +use sha2::Sha256; +use std::sync::Arc; +use tokio::sync::Mutex; +use tokio::sync::RwLock; +use tokio_tungstenite::{WebSocketStream, tungstenite::Message}; +use uuid::Uuid; +use x448::PublicKey; +pub struct CommunityConnection { + pub session: Arc>>, + pub user_id: Arc>>, + pub community: Arc>>>, + identified: Arc>, + challenged: Arc>, + challenge: Arc>, + auth: Arc>>, + pub ping: Arc>, +} +impl CommunityConnection { + pub fn new( + session: WebSocketStream, + community: Arc, + ) -> Arc { + Arc::new(Self { + session: Arc::new(Mutex::new(session)), + user_id: Arc::new(RwLock::new(None)), + community: Arc::new(RwLock::new(Some(community))), + identified: Arc::new(RwLock::new(false)), + challenged: Arc::new(RwLock::new(false)), + challenge: Arc::new(RwLock::new(String::new())), + auth: Arc::new(RwLock::new(None)), + ping: Arc::new(RwLock::new(-1)), + }) + } + pub async fn send_message(&self, message: &CommunicationValue) { + let mut session = self.session.lock().await; + session + .send(Message::Text(message.to_json().to_string())) + .await + .unwrap(); + } + pub async fn get_community(&self) -> Option> { + self.community.read().await.clone() + } + pub async fn get_user_id(&self) -> Option { + *self.user_id.read().await + } + pub async fn is_identified(&self) -> bool { + *self.identified.read().await && *self.challenged.read().await + } + + pub async fn handle_message(self: Arc, message: String) { + let cv = CommunicationValue::from_json(&message); + + if cv.is_type(CommunicationType::identification) && !self.is_identified().await { + self.handle_identification(cv).await; + return; + } + + if cv.is_type(CommunicationType::challenge_response) && !self.is_identified().await { + self.handle_challenge_response(cv).await; + return; + } + + if !self.is_identified().await { + return; + } + + if cv.is_type(CommunicationType::ping) { + self.handle_ping(cv).await; + return; + } + + if cv.is_type(CommunicationType::client_changed) { + //self.handle_client_changed(cv).await; + return; + } + + if cv.is_type(CommunicationType::function) { + self.handle_function(cv).await; + return; + } + } + async fn handle_function(&self, cv: CommunicationValue) { + /* + * { + * "type": "function", + * "id": "", + * "log": { + * "log_level": 0, + * "message": "running function" + * }, + * "data": { + * "codec": "", // optional + * "name": "", + * "path": "/...", + * "function": "", + * "payload": { + * "": "", + * ... + * } + * } + * } + */ + let name = cv.get_data(DataTypes::name).unwrap().as_str().unwrap(); + let path = cv.get_data(DataTypes::path).unwrap().as_str().unwrap(); + let function = cv.get_data(DataTypes::function).unwrap().as_str().unwrap(); + + let result = self + .get_community() + .await + .unwrap() + .run_function(self.get_user_id().await.unwrap(), name, path, function, &cv) + .await; + + self.send_message(&result).await; + } + async fn handle_identification(&self, cv: CommunicationValue) { + let user_id = match cv.get_data(DataTypes::user_id) { + Some(id_str) => match Uuid::parse_str(&id_str.to_string()) { + Ok(id) => id, + Err(_) => { + self.send_error_response( + &cv.get_id(), + CommunicationType::error_invalid_user_id, + ) + .await; + return; + } + }, + None => { + self.send_error_response(&cv.get_id(), CommunicationType::error_invalid_user_id) + .await; + return; + } + }; + + let Some(user) = get_user(user_id).await else { + self.send_error_response(&cv.get_id(), CommunicationType::error_invalid_user_id) + .await; + return; + }; + + { + let mut auth_guard = self.auth.write().await; + *auth_guard = Some(user.clone()); + + let mut user_id_guard = self.user_id.write().await; + *user_id_guard = Some(user_id); + + let mut identified_guard = self.identified.write().await; + *identified_guard = true; + } + + let challenge_str: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(32) + .map(char::from) + .collect(); + + { + let mut challenge_guard = self.challenge.write().await; + *challenge_guard = challenge_str.clone(); + } + + let user_public_key_bytes = match STANDARD.decode(&user.public_key) { + Ok(bytes) => bytes, + Err(_) => { + self.send_error_response(&cv.get_id(), CommunicationType::error_invalid_user_id) + .await; + return; + } + }; + + let user_pub_key: PublicKey = match PublicKey::from_bytes(&user_public_key_bytes) { + Some(key) => key, + None => { + self.send_error_response(&cv.get_id(), CommunicationType::error_invalid_user_id) + .await; + return; + } + }; + + let Some(community) = self.community.read().await.clone() else { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + }; + + let community_private_key = community.get_private_key(); + let community_public_key = community.get_public_key(); + + let shared_secret = match community_private_key.to_diffie_hellman(&user_pub_key) { + Some(secret) => secret, + None => { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + } + }; + + let aes_key = { + let hk = Hkdf::::new(None, shared_secret.as_bytes()); + let mut key_bytes = [0u8; 32]; + hk.expand(b"challenge", &mut key_bytes) + .expect("HKDF failure"); + key_bytes + }; + + let cipher = Aes256Gcm::new_from_slice(&aes_key).expect("AES init failed"); + + let nonce_bytes: [u8; 12] = rand::random(); + let nonce = Nonce::from_slice(&nonce_bytes); + + let encrypted_challenge = match cipher.encrypt(nonce, challenge_str.as_bytes()) { + Ok(data) => data, + Err(_) => { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + } + }; + + let mut encrypted_out = nonce_bytes.to_vec(); + encrypted_out.extend(encrypted_challenge); + + let response = CommunicationValue::new(CommunicationType::challenge) + .add_data_str( + DataTypes::public_key, + STANDARD.encode(community_public_key.as_bytes()), + ) + .add_data_str(DataTypes::challenge, STANDARD.encode(&encrypted_out)) + .with_id(cv.get_id()); + + self.send_message(&response).await; + } + async fn handle_challenge_response(self: Arc, cv: CommunicationValue) { + let client_challenge_response_b64 = match cv.get_data(DataTypes::challenge) { + Some(data) => data.to_string(), + None => { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + } + }; + + let challenge_response_bytes = match STANDARD.decode(&client_challenge_response_b64) { + Ok(bytes) => bytes, + Err(_) => { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + } + }; + + if challenge_response_bytes.len() < 12 { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + } + + let Some(user) = self.auth.read().await.clone() else { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + }; + + let Some(user_pub_bytes) = STANDARD.decode(&user.public_key).ok() else { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + }; + + let Some(user_pub_key) = PublicKey::from_bytes(&user_pub_bytes) else { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + }; + + let Some(community) = self.community.read().await.clone() else { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + }; + + let community_private_key = community.get_private_key(); + + let shared_secret = match community_private_key.to_diffie_hellman(&user_pub_key) { + Some(secret) => secret, + None => { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + } + }; + + let aes_key = { + use hkdf::Hkdf; + use sha2::Sha256; + + let hk = Hkdf::::new(None, shared_secret.as_bytes()); + let mut key_bytes = [0u8; 32]; + hk.expand(b"challenge", &mut key_bytes) + .expect("HKDF failure"); + key_bytes + }; + + let (nonce_bytes, ciphertext) = challenge_response_bytes.split_at(12); + let nonce = Nonce::from_slice(nonce_bytes); + let cipher = Aes256Gcm::new_from_slice(&aes_key).expect("AES init failed"); + + let decrypted_bytes = match cipher.decrypt(nonce, ciphertext) { + Ok(pt) => pt, + Err(_) => { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + } + }; + + let client_response = match String::from_utf8(decrypted_bytes) { + Ok(str) => str, + Err(_) => { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + } + }; + + let expected_challenge = self.challenge.read().await.clone(); + + if client_response != expected_challenge { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + self.close().await; + return; + } + + { + let mut authenticated_guard = self.challenged.write().await; + *authenticated_guard = true; + } + + let Some(community) = self.community.read().await.clone() else { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + }; + let arc = Arc::new(community); + + let Some(user_id) = self.get_user_id().await else { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + }; + + arc.add_connection(self.clone()).await; + + let response = CommunicationValue::new(CommunicationType::identification_response) + .add_data(DataTypes::interactables, { + let a: Vec>> = arc.get_interactables(user_id).await; + let mut c: JsonValue = JsonValue::new_object(); + for b in a { + let mut subject = JsonValue::new_object(); + subject["codec"] = JsonValue::String(b.get_codec()); + subject["data"] = b.get_data(); + c[b.get_name()] = subject; + } + c + }) + .with_id(cv.get_id()); + + self.send_message(&response).await; + } + + async fn send_error_response(&self, message_id: &Uuid, error_type: CommunicationType) { + let error = CommunicationValue::new(error_type).with_id(*message_id); + self.send_message(&error).await; + } + pub async fn close(&self) { + let mut session = self.session.lock().await; + let _ = session.close(None).await; + } + pub async fn handle_close(&self) { + if self.is_identified().await { + if let Some(user_id) = self.get_user_id().await { + todo!(); + } + } + } + + async fn handle_ping(&self, cv: CommunicationValue) { + if let Some(last_ping) = cv.get_data(DataTypes::last_ping) { + if let Ok(ping_val) = last_ping.to_string().parse::() { + let mut ping_guard = self.ping.write().await; + *ping_guard = ping_val; + } + } + + let response = CommunicationValue::new(CommunicationType::pong).with_id(cv.get_id()); + + self.send_message(&response).await; + } +} diff --git a/src/communities/community_manager.rs b/src/communities/community_manager.rs new file mode 100644 index 0000000..ae6acab --- /dev/null +++ b/src/communities/community_manager.rs @@ -0,0 +1,45 @@ +use crate::communities::community::{self, Community}; +use crate::gui::log_panel; +use crate::util::file_util; +use once_cell::sync::Lazy; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::Mutex; +use uuid::Uuid; + +pub static COMMUNITY_REGISTRY: Lazy>>>> = + Lazy::new(|| Arc::new(Mutex::new(HashMap::new()))); + +pub async fn add_community(community: Arc) { + COMMUNITY_REGISTRY + .lock() + .await + .insert(community.get_name().to_string(), community); +} +pub async fn get_community(name: &str) -> Option> { + if let Some(c) = COMMUNITY_REGISTRY.lock().await.get(name) { + Some(c.clone()) + } else { + None + } +} + +pub async fn load_communities() { + let community_names = file_util::get_children("communities"); + for name in community_names { + if let Some(community) = community::load(&name).await { + add_community(community).await; + } else { + log_panel::log_message(format!("failed to load the {} community", &name)); + } + } +} +pub async fn save_communities() { + for community in COMMUNITY_REGISTRY.lock().await.values() { + community.save().await; + } +} + +pub async fn get_communities() -> Vec> { + COMMUNITY_REGISTRY.lock().await.values().cloned().collect() +} diff --git a/src/communities/community_socket.rs b/src/communities/community_socket.rs new file mode 100644 index 0000000..c095bd2 --- /dev/null +++ b/src/communities/community_socket.rs @@ -0,0 +1,67 @@ +use crate::communities::{ + community::Community, community_connection::CommunityConnection, community_manager, +}; +use futures::StreamExt; +use std::sync::Arc; +use tokio::net::TcpListener; +use tokio_tungstenite::accept_hdr_async; +use tungstenite::handshake::server::{Request, Response}; + +pub async fn start(port: u16) -> bool { + let listener = TcpListener::bind(format!("0.0.0.0:{}", port)).await; + if let Err(_) = listener { + return false; + } + let listener = listener.unwrap(); + tokio::spawn(async move { + while let Ok((stream, _)) = listener.accept().await { + tokio::spawn(async move { + let mut path: String = "/".to_string(); + let callback = |req: &Request, response: Response| { + path = format!("{}", &req.uri().path()); + Ok(response) + }; + let ws_stream = match accept_hdr_async(stream, callback).await { + Ok(ws) => ws, + Err(e) => { + return; + } + }; + if path.starts_with("/community/") { + let community_id = path.split("/").nth(2).unwrap(); + if let Some(community) = community_manager::get_community(community_id).await { + let community_conn: Arc = + Arc::from(CommunityConnection::new(ws_stream, community)); + loop { + let msg_result = { + let mut session_lock = community_conn.session.lock().await; + session_lock.next().await + }; + + match msg_result { + Some(Ok(msg)) => { + if msg.is_text() { + let text = msg.into_text().unwrap(); + community_conn.clone().handle_message(text).await; + } else if msg.is_close() { + community_conn.handle_close().await; + return; + } + } + Some(Err(e)) => { + community_conn.handle_close().await; + return; + } + None => { + community_conn.handle_close().await; + return; + } + } + } + } + } + }); + } + }); + true +} diff --git a/src/communities/interactables/category.rs b/src/communities/interactables/category.rs new file mode 100644 index 0000000..e352022 --- /dev/null +++ b/src/communities/interactables/category.rs @@ -0,0 +1,108 @@ +use crate::{ + communities::{community::Community, interactables::interactable::Interactable}, + data::communication::{CommunicationType, CommunicationValue}, +}; +use axum::Json; +use json::JsonValue; +use std::any::Any; +use std::sync::Arc; + +pub struct Category { + name: String, + path: String, + community: Arc, + children: Vec>>, +} +impl Category { + pub fn new() -> Category { + Category { + name: String::new(), + path: String::new(), + community: Arc::new(Community::new()), + children: Vec::new(), + } + } + pub fn get_child(&self, path: String, name: String) -> Option>> { + if path.is_empty() { + self.children + .iter() + .find(|child| child.get_name() == &name) + .cloned() + } else { + let sub_module = path.split("/").next().unwrap(); + let next = self + .children + .iter() + .find(|child| child.get_name() == sub_module) + .unwrap(); + if next.get_codec() == "category" { + let next_cat = next.as_any().downcast_ref::().unwrap(); + next_cat.get_child(path, name) + } else { + Some(next.clone()) + } + } + } + pub fn get_children(&self) -> Vec>> { + self.children.iter().map(|child| child.clone()).collect() + } +} + +impl Interactable for Category { + fn as_any(&self) -> &dyn Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + fn get_codec(&self) -> String { + "category".to_string() + } + fn set_name(&mut self, name: String) { + self.name = name; + } + fn set_path(&mut self, path: String) { + self.path = path; + } + fn get_community(&self) -> &Arc { + &self.community + } + fn set_community(&mut self, community: Arc) { + self.community = community; + } + fn get_name(&self) -> &String { + &self.name + } + fn get_path(&self) -> &String { + &self.path + } + fn get_total_path(&self) -> String { + String::new() + &self.path + "/" + &self.name + } + fn get_data(&self) -> JsonValue { + let mut v = JsonValue::new_object(); + for child in &self.children { + let mut subject = JsonValue::new_object(); + subject["codec"] = JsonValue::String(child.get_codec()); + subject["data"] = child.get_data(); + v[child.get_name()] = subject; + } + v + } + fn run_function(&self, cv: CommunicationValue) -> CommunicationValue { + CommunicationValue::new(CommunicationType::error) + } + fn to_json(&self) -> JsonValue { + let mut v = JsonValue::new_object(); + v["children"] = JsonValue::new_array(); + for child in &self.children { + v["children"].push(child.to_json()); + } + v + } + fn load(&mut self, community: Arc, path: String, name: String, json: &JsonValue) { + self.community = community; + self.name = name; + self.path = path; + } +} diff --git a/src/communities/interactables/interactable.rs b/src/communities/interactables/interactable.rs new file mode 100644 index 0000000..db99ebd --- /dev/null +++ b/src/communities/interactables/interactable.rs @@ -0,0 +1,27 @@ +use crate::{ + communities::community::Community, + data::communication::{CommunicationType, CommunicationValue}, +}; +use axum::Json; +use json::JsonValue; +use std::any::Any; +use std::sync::Arc; + +pub type InteractableFactory = fn() -> Box; + +pub trait Interactable: Send + Sync + Any { + fn as_any(&self) -> &dyn Any; + fn as_any_mut(&mut self) -> &mut dyn Any; + fn get_codec(&self) -> String; + fn get_name(&self) -> &String; + fn get_path(&self) -> &String; + fn get_total_path(&self) -> String; + fn set_name(&mut self, name: String); + fn set_path(&mut self, path: String); + fn get_community(&self) -> &Arc; + fn set_community(&mut self, community: Arc); + fn run_function(&self, cv: CommunicationValue) -> CommunicationValue; + fn get_data(&self) -> JsonValue; + fn to_json(&self) -> JsonValue; + fn load(&mut self, community: Arc, path: String, name: String, json: &JsonValue); +} diff --git a/src/communities/interactables/registry.rs b/src/communities/interactables/registry.rs new file mode 100644 index 0000000..d9f6954 --- /dev/null +++ b/src/communities/interactables/registry.rs @@ -0,0 +1,73 @@ +use crate::communities::community::Community; +use crate::communities::interactables::category::Category; +use crate::communities::interactables::interactable::{Interactable, InteractableFactory}; +use crate::communities::interactables::text_chat::TextChat; +use crate::communities::interactables::voice_chat::VoiceChat; +use crate::gui::log_panel; +use crate::util::file_util; +use json::JsonValue; +use once_cell::sync::Lazy; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::Mutex; + +pub static INTERACTABLE_REGISTRY: Lazy>>> = + Lazy::new(|| Arc::new(Mutex::new(HashMap::new()))); +pub async fn load_interactables() { + INTERACTABLE_REGISTRY + .lock() + .await + .insert(TextChat::new().get_codec(), || { + Box::new(TextChat::new()) as Box + }); + INTERACTABLE_REGISTRY + .lock() + .await + .insert(VoiceChat::new().get_codec(), || { + Box::new(VoiceChat::new()) as Box + }); + INTERACTABLE_REGISTRY + .lock() + .await + .insert(Category::new().get_codec(), || { + Box::new(Category::new()) as Box + }); +} +pub async fn register_interactable(name: String, interactable: InteractableFactory) { + INTERACTABLE_REGISTRY + .lock() + .await + .insert(name.to_string(), interactable) + .unwrap(); +} +pub async fn get_interactable(name: &str) -> Box { + INTERACTABLE_REGISTRY.lock().await.get(name).unwrap()() +} +pub async fn save(interactable: &Arc>) { + let mut json_object: JsonValue = interactable.to_json().clone(); + json_object["codec"] = JsonValue::String(interactable.get_codec()); + file_util::save_file( + &format!( + "communities/{}/interactables/{}", + interactable.get_community().get_name(), + interactable.get_path() + ), + &format!("{}.json", interactable.get_name()), + &json_object.to_string(), + ); +} +pub async fn load( + c: Arc, + path: String, + name: String, +) -> Box { + let s = file_util::load_file( + &format!("communities/{}/interactables/{}", c.get_name(), path), + &format!("{}.json", name), + ); + let json_object: JsonValue = json::parse(&s).unwrap(); + let codec: String = json_object["codec"].as_str().unwrap().to_string(); + let mut interactable = get_interactable(&codec).await; + interactable.load(c, path, name, &json_object); + interactable +} diff --git a/src/communities/interactables/text_chat.rs b/src/communities/interactables/text_chat.rs new file mode 100644 index 0000000..5a3b2da --- /dev/null +++ b/src/communities/interactables/text_chat.rs @@ -0,0 +1,225 @@ +use crate::{ + communities::{community::Community, interactables::interactable::Interactable}, + data::communication::{CommunicationType, CommunicationValue, DataTypes}, + gui::log_panel::log_message, + util::file_util::{get_children, load_file, save_file}, +}; +use aes_gcm::aead::Payload; +use axum::Json; +use json::{JsonValue, array, object}; +use std::any::Any; +use std::fs::{self, File}; +use std::sync::Arc; +use uuid::Uuid; +pub struct TextChat { + name: String, + path: String, + community: Arc, +} +impl TextChat { + pub fn new() -> TextChat { + TextChat { + name: String::new(), + path: String::new(), + community: Arc::new(Community::new()), + } + } + pub fn add_message(&self, send_time: u128, sender: Uuid, message: &str) { + let user_dir = &format!( + "communities/{}/interactables/{}/{}", + self.get_community().get_name(), + self.get_path(), + self.get_name() + ); + + if let Err(e) = fs::create_dir_all(user_dir) { + log_message(format!("Failed to create chat directory: {}", e)); + return; + } + + let mut chunk_index = 0; + let mut message_chunk = array![]; + + // find latest chunk not full (max 800 msgs) + loop { + let file_name = format!("msgs_{}.json", chunk_index); + let file_content = load_file(&user_dir, &file_name); + + if !file_content.is_empty() { + if let Ok(current_chunk) = json::parse(&file_content) { + if current_chunk.is_array() && current_chunk.len() < 800 { + message_chunk = current_chunk; + break; + } + } else { + log_message(format!("Failed to parse existing JSON file: {}", file_name)); + } + } else { + // New file, use empty array + break; + } + + chunk_index += 1; + if chunk_index > 1000 { + log_message(format!("Too many message chunks. Aborting add.")); + return; + } + } + + let json_obj = object! { + "timestamp" => send_time as i64, + "content" => message, + "sender" => sender.to_string(), + }; + + if let Err(e) = message_chunk.push(json_obj) { + log_message(format!("Failed to push new message into JSON array: {}", e)); + return; + } + + let file_name = format!("msgs_{}.json", chunk_index); + log_message(format!("Saving message to {}/{}", user_dir, file_name)); + save_file(&user_dir, &file_name, &message_chunk.dump()); + } + pub fn get_messages(&self, loaded_messages: i64, amount: i64) -> JsonValue { + let mut messages = array![]; + + let mut latest_chunk_index: i32 = -1; + let files = get_children(&format!( + "communities/{}/interactables/{}/{}", + self.get_community().get_name(), + self.get_path(), + self.get_name() + )); + + for entry in files { + if let Some(num) = { + entry + .strip_prefix("msgs_") + .and_then(|s| s.strip_suffix(".json")) + } { + if let Ok(index) = num.parse::() { + if index > latest_chunk_index { + latest_chunk_index = index; + } + } + } + } + + if latest_chunk_index == -1 { + return messages; + } + + let mut to_skip = loaded_messages; + let mut needed = amount; + + for chunk_index in (0..=latest_chunk_index).rev() { + if needed == 0 { + break; + } + let file_name = format!("msgs_{}.json", chunk_index); + let file_content = load_file( + &format!( + "communities/{}/interactables/{}/{}", + self.get_community().get_name(), + self.get_path(), + self.get_name() + ), + &file_name, + ); + if file_content.is_empty() { + continue; + } + if let Ok(chunk) = json::parse(&file_content) { + for i in (0..chunk.len()).rev() { + if needed == 0 { + break; + } + if to_skip > 0 { + to_skip -= 1; + continue; + } + messages.push(chunk[i].clone()).unwrap(); + needed -= 1; + } + } + } + + messages + } +} +impl Interactable for TextChat { + fn as_any(&self) -> &dyn Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + fn get_codec(&self) -> String { + "text".to_string() + } + fn set_name(&mut self, name: String) { + self.name = name; + } + fn set_path(&mut self, path: String) { + self.path = path; + } + fn get_community(&self) -> &Arc { + &self.community + } + fn set_community(&mut self, community: Arc) { + self.community = community; + } + fn get_name(&self) -> &String { + &self.name + } + fn get_path(&self) -> &String { + &self.path + } + fn get_total_path(&self) -> String { + String::new() + &self.path + "/" + &self.name + } + fn get_data(&self) -> JsonValue { + JsonValue::new_object() + } + fn run_function(&self, cv: CommunicationValue) -> CommunicationValue { + let payload = cv.get_data(DataTypes::payload).unwrap(); + if cv.get_data(DataTypes::function).unwrap().as_str().unwrap() == "get_messages" { + let amount = payload["amount"].as_i64().unwrap(); + let loaded_messages = payload["loaded_messages"].as_i64().unwrap(); + let messages = self.get_messages(loaded_messages, amount); + let mut payload = JsonValue::new_object(); + payload["messages"] = messages; + return CommunicationValue::new(CommunicationType::function) + .with_id(cv.get_id()) + .add_data_str(DataTypes::name, self.name.clone()) + .add_data_str(DataTypes::path, self.path.clone()) + .add_data_str(DataTypes::result, "message_chunk".to_string()) + .add_data(DataTypes::payload, payload); + } + if cv.get_data(DataTypes::function).unwrap().as_str().unwrap() == "send_message" { + let message = payload["message"].as_str().unwrap(); + let milliseconds_timestamp: u128 = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis(); + self.add_message(milliseconds_timestamp, cv.get_sender().unwrap(), message); + return CommunicationValue::new(CommunicationType::function) + .with_id(cv.get_id()) + .add_data_str(DataTypes::name, self.name.clone()) + .add_data_str(DataTypes::path, self.path.clone()) + .add_data_str(DataTypes::result, "message_received".to_string()) + .add_data(DataTypes::payload, JsonValue::new_object()); + } + CommunicationValue::new(CommunicationType::error).with_id(cv.get_id()) + } + fn to_json(&self) -> JsonValue { + let mut v = JsonValue::new_object(); + v + } + fn load(&mut self, community: Arc, path: String, name: String, json: &JsonValue) { + self.community = community; + self.name = name; + self.path = path; + } +} diff --git a/src/communities/interactables/voice_chat.rs b/src/communities/interactables/voice_chat.rs new file mode 100644 index 0000000..5841e24 --- /dev/null +++ b/src/communities/interactables/voice_chat.rs @@ -0,0 +1,83 @@ +use crate::{ + communities::{community::Community, interactables::interactable::Interactable}, + data::communication::{CommunicationType, CommunicationValue}, +}; +use json::JsonValue; +use std::any::Any; +use std::sync::Arc; +use uuid::Uuid; +pub enum CallUserState { + Active, + Muted, + Deafed, +} + +pub struct CallUser { + user_id: Uuid, + user_state: CallUserState, + streaming: bool, +} + +pub struct VoiceChat { + name: String, + path: String, + community: Arc, + users: Vec, +} +impl VoiceChat { + pub fn new() -> VoiceChat { + VoiceChat { + name: String::new(), + path: String::new(), + community: Arc::new(Community::new()), + users: Vec::new(), + } + } +} +impl Interactable for VoiceChat { + fn as_any(&self) -> &dyn Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + fn get_codec(&self) -> String { + "voice".to_string() + } + fn set_name(&mut self, name: String) { + self.name = name; + } + fn set_path(&mut self, path: String) { + self.path = path; + } + fn get_community(&self) -> &Arc { + &self.community + } + fn set_community(&mut self, community: Arc) { + self.community = community; + } + fn get_name(&self) -> &String { + &self.name + } + fn get_path(&self) -> &String { + &self.path + } + fn get_total_path(&self) -> String { + String::new() + &self.path + "/" + &self.name + } + fn get_data(&self) -> JsonValue { + JsonValue::new_object() + } + fn run_function(&self, cv: CommunicationValue) -> CommunicationValue { + CommunicationValue::new(CommunicationType::error) + } + fn to_json(&self) -> JsonValue { + let mut v = JsonValue::new_object(); + v + } + fn load(&mut self, community: Arc, path: String, name: String, json: &JsonValue) { + self.community = community; + self.name = name; + self.path = path; + } +} diff --git a/src/communities/mod.rs b/src/communities/mod.rs new file mode 100644 index 0000000..dbc9534 --- /dev/null +++ b/src/communities/mod.rs @@ -0,0 +1,11 @@ +pub mod community_manager; +pub mod community_socket; +pub mod interactables { + pub mod category; + pub mod interactable; + pub mod registry; + pub mod text_chat; + pub mod voice_chat; +} +pub mod community; +pub mod community_connection; diff --git a/src/data/communication.rs b/src/data/communication.rs index 20d445c..4612083 100644 --- a/src/data/communication.rs +++ b/src/data/communication.rs @@ -1,9 +1,6 @@ -use axum::Json; use json::number::Number; -use json::{Array, JsonValue, object, parse, stringify}; -use std::any::Any; +use json::{Array, JsonValue, object, parse}; use std::collections::HashMap; -use std::env::VarsOs; use std::str::FromStr; use std::time::{SystemTime, UNIX_EPOCH}; use uuid::Uuid; @@ -157,6 +154,9 @@ impl DataTypes { #[derive(PartialEq, Clone, Debug)] pub enum CommunicationType { error, + error_invalid_user_id, + error_not_found, + error_invalid_challenge, success, message, message_send, @@ -213,7 +213,7 @@ impl CommunicationType { "messageotheriota" => CommunicationType::message_other_iota, "messagechunk" => CommunicationType::message_chunk, "messagesget" => CommunicationType::messages_get, - "message_send" => CommunicationType::message_send, + "messagesend" => CommunicationType::message_send, "changeconfirm" => CommunicationType::change_confirm, "confirmreceive" => CommunicationType::confirm_receive, "confirmread" => CommunicationType::confirm_read, @@ -259,8 +259,8 @@ impl CommunicationType { pub struct CommunicationValue { pub id: Uuid, pub comm_type: CommunicationType, - pub sender: Uuid, - pub receiver: Uuid, + pub sender: Option, + pub receiver: Option, pub data: HashMap, } @@ -269,8 +269,8 @@ impl CommunicationValue { Self { id: Uuid::new_v4(), comm_type, - sender: Uuid::new_v4(), - receiver: Uuid::new_v4(), + sender: None, + receiver: None, data: HashMap::new(), } } @@ -282,17 +282,17 @@ impl CommunicationValue { self.id.clone() } pub fn with_sender(mut self, sender: Uuid) -> Self { - self.sender = sender; + self.sender = Some(sender); self } - pub fn get_sender(&self) -> Uuid { + pub fn get_sender(&self) -> Option { self.sender.clone() } pub fn with_receiver(mut self, receiver: Uuid) -> Self { - self.receiver = receiver; + self.receiver = Some(receiver); self } - pub fn get_receiver(&self) -> Uuid { + pub fn get_receiver(&self) -> Option { self.receiver.clone() } pub fn add_data_num(mut self, key: DataTypes, value: Number) -> Self { @@ -323,13 +323,34 @@ impl CommunicationValue { for (k, v) in &self.data { jdata[&format!("{:?}", k)] = JsonValue::from(v.clone()); } - - object! { - id: self.id.to_string(), - type: format!("{:?}", self.comm_type), - sender: self.sender.to_string(), - receiver: self.receiver.to_string(), - data: jdata + if self.sender.is_some() && self.receiver.is_some() { + object! { + id: self.id.to_string(), + type: format!("{:?}", self.comm_type), + sender: self.sender.unwrap().to_string(), + receiver: self.receiver.unwrap().to_string(), + data: jdata + } + } else if self.sender.is_some() { + object! { + id: self.id.to_string(), + type: format!("{:?}", self.comm_type), + sender: self.sender.unwrap().to_string(), + data: jdata + } + } else if self.receiver.is_some() { + object! { + id: self.id.to_string(), + type: format!("{:?}", self.comm_type), + receiver: self.receiver.unwrap().to_string(), + data: jdata + } + } else { + object! { + id: self.id.to_string(), + type: format!("{:?}", self.comm_type), + data: jdata + } } } @@ -337,15 +358,24 @@ impl CommunicationValue { let parsed = parse(json_str).unwrap(); let comm_type = CommunicationType::parse(parsed["type"].to_string()); - - let sender: Uuid = parsed["sender"] - .as_str() - .and_then(|s| Uuid::parse_str(s).ok()) - .unwrap_or(Uuid::new_v4()); - let receiver: Uuid = parsed["receiver"] - .as_str() - .and_then(|s| Uuid::parse_str(s).ok()) - .unwrap_or(Uuid::new_v4()); + let mut sender: Option = None; + if parsed.has_key("sender") { + sender = Some( + parsed["sender"] + .as_str() + .and_then(|s| Uuid::parse_str(s).ok()) + .unwrap_or(Uuid::new_v4()), + ); + } + let mut receiver: Option = None; + if parsed.has_key("receiver") { + receiver = Some( + parsed["receiver"] + .as_str() + .and_then(|s| Uuid::parse_str(s).ok()) + .unwrap_or(Uuid::new_v4()), + ); + } let uuid = Uuid::parse_str(parsed["id"].as_str().unwrap_or("")).unwrap_or(Uuid::new_v4()); let mut data = HashMap::new(); @@ -386,17 +416,22 @@ impl CommunicationValue { .unwrap() .as_millis() as i64; - let cv = CommunicationValue::new(CommunicationType::message_other_iota) + let sender = original.get_sender(); + CommunicationValue::new(CommunicationType::message_other_iota) .with_id(original.get_id()) .with_receiver(receiver.unwrap()) + .add_data( DataTypes::receiver_id, + JsonValue::String(receiver.unwrap().to_string()), + ) + .with_sender(sender.unwrap()) .add_data(DataTypes::send_time, JsonValue::String(now_ms.to_string())) + .add_data( + DataTypes::sender_id, + JsonValue::String(sender.unwrap().to_string()), + ) .add_data( DataTypes::content, JsonValue::String(original.get_data(DataTypes::content).unwrap().to_string()), - ); - - // include sender_id if the original had one - let sender = original.get_sender(); - cv.add_data(DataTypes::sender_id, JsonValue::String(sender.to_string())) + ) } } diff --git a/src/langu/language_creator.rs b/src/langu/language_creator.rs index 9b2c9c3..5f45c47 100644 --- a/src/langu/language_creator.rs +++ b/src/langu/language_creator.rs @@ -29,7 +29,18 @@ pub fn create_languages() { general_texts.insert("user_id", "USER ID: {}"); general_texts.insert("user_ids", "USER IDS: {}"); general_texts.insert("setup_completed", "Launched"); - + general_texts.insert( + "community_active", + "Communities active on ws://0.0.0.0:{}/community/...", + ); + general_texts.insert( + "community_start_error", + "Failed to start community socket on port {}!", + ); + general_texts.insert( + "community_start_error_admin", + "Failed to start community socket on port {}! Run with admin privileges", + ); // DEBUG debug_messages.insert("", ""); save_file( diff --git a/src/langu/language_manager.rs b/src/langu/language_manager.rs index ca945f1..1ca84f7 100644 --- a/src/langu/language_manager.rs +++ b/src/langu/language_manager.rs @@ -1,5 +1,5 @@ -use crate::util::file_util::{self, load_file}; -use json::{JsonValue, parse}; +use crate::util::file_util::{self}; +use json::parse; use once_cell::sync::Lazy; use std::collections::HashMap; use std::sync::Mutex; diff --git a/src/main.rs b/src/main.rs index 3a0be16..61b1fa0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,12 @@ -use json::JsonValue::String; -use json::{self}; +use json::{self, JsonValue::String}; use std::sync::Arc; use std::sync::LazyLock; use std::sync::Mutex; -use sysinfo::User; use tokio::time::{Duration, sleep}; use uuid::Uuid; mod auth; +mod communities; mod data; mod eula; mod gui; @@ -16,13 +15,16 @@ mod omikron; mod users; mod util; +use crate::communities::interactables::registry; +use crate::communities::{community_manager, community_socket}; use crate::data::communication::{CommunicationType, CommunicationValue, DataTypes}; use crate::gui::app_state::AppState; use crate::gui::log_panel::{log_message, log_message_trans}; use crate::gui::{log_panel, ratatui_interface}; use crate::langu::language_creator; +use crate::langu::language_manager::format; use crate::omikron::omikron_connection::OmikronConnection; -use crate::users::user_manager::UserManager; +use crate::users::user_manager; use crate::util::config_util::CONFIG; pub static APP_STATE: LazyLock>> = @@ -50,9 +52,9 @@ async fn main() { } // USER MANAGEMENT - UserManager::load_users().await; + user_manager::load_users().await; let mut sb = "".to_string(); - for up in UserManager::get_users() { + for up in user_manager::get_users() { sb = sb + "," + &up.user_id.to_string().as_str(); } @@ -72,6 +74,32 @@ async fn main() { .unwrap() )); log_message(format!("User IDS: {}", sb)); + + // COMMUNITY MANAGEMENT + registry::load_interactables().await; + community_manager::load_communities().await; + community_manager::save_communities().await; + let mut sb1 = "".to_string(); + for cp in community_manager::get_communities().await { + sb1 = sb1 + "," + &cp.get_name().to_string().as_str(); + } + + if !sb1.is_empty() { + sb1.remove(0); + sb1 = sb1 + ","; + } + log_message(format!("Community IDS: {}", sb1)); + let port = CONFIG.lock().unwrap().get_port(); + if community_socket::start(port).await { + log_message(format("community_active", &[&port.to_string()])); + } else { + if port < 1024 { + log_message(format("community_start_error_admin", &[&port.to_string()])); + } else { + log_message(format("community_start_error", &[&port.to_string()])); + } + } + loop { let omikron: OmikronConnection = OmikronConnection::new(); omikron.connect().await; diff --git a/src/omikron/omikron_connection.rs b/src/omikron/omikron_connection.rs index 320a4cf..584851e 100644 --- a/src/omikron/omikron_connection.rs +++ b/src/omikron/omikron_connection.rs @@ -1,9 +1,8 @@ use crate::data::communication::{CommunicationType, CommunicationValue, DataTypes}; -use crate::gui::log_panel::log_message_trans; use crate::gui::log_panel::{log_cv, log_message}; use crate::users::contact::Contact; use crate::users::user_community_util::UserCommunityUtil; -use crate::util::chat_files::ChatFiles; +use crate::util::chat_files; use crate::util::chats_util::{get_user, get_users, mod_user}; use futures_util::{SinkExt, StreamExt}; use json::JsonValue; @@ -33,6 +32,7 @@ pub struct OmikronConnection { >, waiting: Arc>>>, // waiting for responses pingpong: Arc>>>, // ping-pong handler + pub last_ping: Arc>, pub message_send_times: Arc>>, pub is_connected: Arc>, } @@ -43,6 +43,7 @@ impl OmikronConnection { writer: Arc::new(Mutex::new(None)), waiting: Arc::new(Mutex::new(HashMap::new())), pingpong: Arc::new(Mutex::new(None)), + last_ping: Arc::new(Mutex::new(-1)), message_send_times: Arc::new(Mutex::new(HashMap::new())), is_connected: Arc::new(Mutex::new(false)), } @@ -112,14 +113,14 @@ impl OmikronConnection { // ************************************************ // log_cv(&cv); if cv.is_type(CommunicationType::message_other_iota) { - let sender_id = &cv.get_sender(); - let receiver_id = &cv.get_receiver(); + let sender_id = &cv.get_sender().unwrap(); + let receiver_id = &cv.get_receiver().unwrap(); - ChatFiles::add_message( + chat_files::add_message( cv.get_data(DataTypes::send_time) .unwrap() .as_i64() - .unwrap_or(-1), + .unwrap_or(0) as u128, false, *receiver_id, *sender_id, @@ -127,7 +128,7 @@ impl OmikronConnection { ); let response = CommunicationValue::new(CommunicationType::message_live) .with_id(cv.get_id()) - .with_receiver(cv.get_receiver()) + .with_receiver(cv.get_receiver().unwrap()) .add_data( DataTypes::send_time, cv.get_data(DataTypes::send_time).unwrap().clone(), @@ -138,7 +139,7 @@ impl OmikronConnection { ) .add_data( DataTypes::sender_id, - JsonValue::String(cv.get_sender().clone().to_string()), + JsonValue::String(cv.get_sender().unwrap().to_string()), ); Self::send_message_static( &writer.clone(), @@ -161,16 +162,16 @@ impl OmikronConnection { ], "content": "" */ - let my_id = cv.get_sender(); + let my_id = cv.get_sender().unwrap(); let other_id = Uuid::from_str( &*cv.get_data(DataTypes::receiver_id).unwrap().to_string(), ) .unwrap(); - ChatFiles::add_message( + chat_files::add_message( SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() - .as_millis() as i64, + .as_millis() as u128, true, my_id, other_id, @@ -189,7 +190,7 @@ impl OmikronConnection { } if cv.is_type(CommunicationType::messages_get) { - let my_id = cv.get_sender(); + let my_id = cv.get_sender().unwrap(); let partner_id = Uuid::from_str( &*cv.get_data(DataTypes::user_id).unwrap().to_string(), ) @@ -207,7 +208,7 @@ impl OmikronConnection { .parse::() .unwrap_or(0); let messages = - ChatFiles::get_messages(my_id, partner_id, offset, amount); + chat_files::get_messages(my_id, partner_id, offset, amount); let resp = CommunicationValue::new(CommunicationType::messages_get) .with_id(cv.get_id()) .with_receiver(my_id) @@ -219,7 +220,7 @@ impl OmikronConnection { } if cv.is_type(CommunicationType::get_chats) { - let user_id = cv.get_sender(); + let user_id = cv.get_sender().unwrap(); let users = get_users(user_id); let resp = CommunicationValue::new(CommunicationType::get_chats) .with_id(cv.get_id()) @@ -231,7 +232,7 @@ impl OmikronConnection { } if cv.is_type(CommunicationType::add_chat) { - let user_id = cv.get_sender(); + let user_id = cv.get_sender().unwrap(); let other_id = Uuid::from_str( &*cv.get_data(DataTypes::user_id).unwrap().to_string(), ) @@ -255,7 +256,7 @@ impl OmikronConnection { if cv.is_type(CommunicationType::add_community) { UserCommunityUtil::add_community( - cv.get_sender(), + cv.get_sender().unwrap(), cv.get_data(DataTypes::community_address) .unwrap() .to_string(), @@ -264,7 +265,7 @@ impl OmikronConnection { ); let resp = CommunicationValue::new(CommunicationType::add_community) .with_id(cv.get_id()) - .with_receiver(cv.get_sender()); + .with_receiver(cv.get_sender().unwrap()); Self::send_message_static(&writer.clone(), resp.to_json().to_string()) .await; continue; @@ -273,11 +274,11 @@ impl OmikronConnection { if cv.is_type(CommunicationType::get_communities) { let resp = CommunicationValue::new(CommunicationType::get_communities) .with_id(cv.get_id()) - .with_receiver(cv.get_sender()) + .with_receiver(cv.get_sender().unwrap()) .add_array( DataTypes::communities, - UserCommunityUtil::get_communities(cv.get_sender()), - ); // needs UserCommunityUtil + UserCommunityUtil::get_communities(cv.get_sender().unwrap()), + ); Self::send_message_static(&writer.clone(), resp.to_json().to_string()) .await; continue; @@ -285,14 +286,14 @@ impl OmikronConnection { if cv.is_type(CommunicationType::remove_community) { UserCommunityUtil::remove_community( - cv.get_sender(), + cv.get_sender().unwrap(), cv.get_data(DataTypes::community_address) .unwrap() .to_string(), ); // needs UserCommunityUtil let resp = CommunicationValue::new(CommunicationType::remove_community) .with_id(cv.get_id()) - .with_receiver(cv.get_sender()); + .with_receiver(cv.get_sender().unwrap()); Self::send_message_static(&writer.clone(), resp.to_json().to_string()) .await; continue; diff --git a/src/omikron/ping_pong_task.rs b/src/omikron/ping_pong_task.rs index ee3abe7..f44f483 100644 --- a/src/omikron/ping_pong_task.rs +++ b/src/omikron/ping_pong_task.rs @@ -1,7 +1,9 @@ use crate::APP_STATE; use crate::data::communication::{CommunicationType, CommunicationValue, DataTypes}; use crate::omikron::omikron_connection::OmikronConnection; +use json::JsonValue; use json::number::Number; +use std::sync::Arc; use tokio::time::Instant; use uuid::Uuid; @@ -17,7 +19,10 @@ impl OmikronConnection { pub async fn send_ping_message(&self, uuid: Uuid) { let ping_message = CommunicationValue::new(CommunicationType::ping) .with_id(uuid) - .add_data_num(DataTypes::last_ping, Number::from(2)) + .add_data_num( + DataTypes::last_ping, + Number::from(*self.last_ping.lock().await), + ) .to_json() .to_string(); @@ -33,11 +38,13 @@ impl OmikronConnection { }; if let Some(send_time) = send_time_opt { - let ping = Instant::now().duration_since(send_time).as_millis() as f64; + let ping = Instant::now().duration_since(send_time).as_millis() as i64; self.message_send_times.lock().await.remove(&id); + *self.last_ping.lock().await = ping as i64; + if log { - APP_STATE.lock().unwrap().push_ping_val(ping); + APP_STATE.lock().unwrap().push_ping_val(ping as f64); } } } diff --git a/src/users/user_manager.rs b/src/users/user_manager.rs index ea45b4c..33ba9df 100644 --- a/src/users/user_manager.rs +++ b/src/users/user_manager.rs @@ -3,142 +3,123 @@ use crate::users::user_profile::UserProfile; use crate::users::user_profile_full::UserProfileFull; use crate::util::config_util::CONFIG; use crate::util::file_util::{load_file, save_file}; -use base64::{Engine as _, engine::general_purpose}; -use der::DerOrd; +use base64::{Engine as _, engine::general_purpose::STANDARD}; use hex; use json::JsonValue; use once_cell::sync::Lazy; -use pkcs8::EncodePublicKey; -use pkcs8::spki::SubjectPublicKeyInfoOwned; -use pkcs8::{ObjectIdentifier, PrivateKeyInfo, SubjectPublicKeyInfo, der::Encode}; use rand::Rng; use rand_core::OsRng; use rand_core::RngCore; -use rustls::pki_types::SubjectPublicKeyInfoDer; use sha2::{Digest, Sha256}; use std::io; use std::sync::Mutex; use uuid::Uuid; use x448::{PublicKey, Secret}; -use x509::AlgorithmIdentifier; - -pub struct UserManager; static USERS: Lazy>> = Lazy::new(|| Mutex::new(Vec::new())); static UNIQUE: Lazy> = Lazy::new(|| Mutex::new(false)); -impl UserManager { - pub async fn create_user(username: &str) -> Option { - let user_id = auth_connector::get_register().await.unwrap(); - let mut buf = [0u8; 56]; - let mut rng = OsRng; - rng.fill_bytes(&mut buf); - let private_key = Secret::from_bytes(&buf).unwrap(); - let public_key = PublicKey::from(&private_key); +pub async fn create_user(username: &str) -> Option { + let user_id = auth_connector::get_register().await.unwrap(); + let mut buf = [0u8; 56]; + let mut rng = OsRng; + rng.fill_bytes(&mut buf); + let private_key = Secret::from_bytes(&buf).unwrap(); + let public_key = PublicKey::from(&private_key); - let mut hasher = Sha256::new(); - hasher.update( - &general_purpose::STANDARD - .encode(&private_key.as_bytes()) - .as_bytes(), - ); - let result = hasher.finalize(); - let private_key_hash = hex::encode(result); + let mut hasher = Sha256::new(); + hasher.update(&STANDARD.encode(&private_key.as_bytes()).as_bytes()); + let result = hasher.finalize(); + let private_key_hash = hex::encode(result); - let mut bytes = [0u8; 192]; - OsRng.fill(bytes.as_mut()); - let reset_token = general_purpose::STANDARD.encode(&bytes); + let mut bytes = [0u8; 192]; + OsRng.fill(bytes.as_mut()); + let reset_token = STANDARD.encode(&bytes); - let up = UserProfile::new( - user_id, - username.to_string(), - None, - general_purpose::STANDARD.encode(&public_key.as_bytes()), - private_key_hash, - reset_token, - ); + let up = UserProfile::new( + user_id, + username.to_string(), + None, + STANDARD.encode(&public_key.as_bytes()), + private_key_hash, + reset_token, + ); - let up_full = UserProfileFull { - user_profile: up.clone(), - private_key: general_purpose::STANDARD.encode(&private_key.as_bytes()), - }; + let up_full = UserProfileFull { + user_profile: up.clone(), + private_key: STANDARD.encode(&private_key.as_bytes()), + }; - auth_connector::complete_register(&up, &CONFIG.lock().unwrap().get_iota_id().to_string()) - .await; - save_file( - "", - &format!("{}.tu", username), - &format!( - "{}::{}", - user_id, - general_purpose::STANDARD.encode(&private_key.as_bytes()) - ), - ); + auth_connector::complete_register(&up, &CONFIG.lock().unwrap().get_iota_id().to_string()).await; + save_file( + "", + &format!("{}.tu", username), + &format!("{}::{}", user_id, STANDARD.encode(&private_key.as_bytes())), + ); - USERS.lock().unwrap().push(up); - Self::save_users().ok(); - Some(up_full) + USERS.lock().unwrap().push(up); + save_users().ok(); + Some(up_full) +} + +pub fn get_user(user_id: Uuid) -> Option { + USERS + .lock() + .unwrap() + .iter() + .cloned() + .find(|u| u.user_id == user_id) +} + +pub fn get_users() -> Vec { + USERS.lock().unwrap().clone() +} + +pub fn add_user(up: UserProfile) { + let mut users = USERS.lock().unwrap(); + users.retain(|u| u.user_id != up.user_id); + users.push(up); + *UNIQUE.lock().unwrap() = true; +} + +pub fn remove_user(user_id: Uuid) { + let mut users = USERS.lock().unwrap(); + users.retain(|u| u.user_id != user_id); + *UNIQUE.lock().unwrap() = true; +} + +pub fn save_users() -> io::Result<()> { + *UNIQUE.lock().unwrap() = false; + let users = USERS.lock().unwrap(); + let arr: Vec = users.iter().map(|u| u.to_json()).collect(); + let json_str = JsonValue::Array(arr).dump(); + + save_file("", "users.json", &json_str); + Ok(()) +} + +pub async fn load_users() -> io::Result<()> { + let content = load_file("", "users.json"); + if content.trim().is_empty() { + return Ok(()); } - pub fn get_user(user_id: Uuid) -> Option { - USERS - .lock() - .unwrap() - .iter() - .cloned() - .find(|u| u.user_id == user_id) - } - - pub fn get_users() -> Vec { - USERS.lock().unwrap().clone() - } - - pub fn add_user(up: UserProfile) { + let 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(); - users.retain(|u| u.user_id != up.user_id); - users.push(up); - *UNIQUE.lock().unwrap() = true; - } - - pub fn remove_user(user_id: Uuid) { - let mut users = USERS.lock().unwrap(); - users.retain(|u| u.user_id != user_id); - *UNIQUE.lock().unwrap() = true; - } - - pub fn save_users() -> io::Result<()> { - *UNIQUE.lock().unwrap() = false; - let users = USERS.lock().unwrap(); - let arr: Vec = users.iter().map(|u| u.to_json()).collect(); - let json_str = JsonValue::Array(arr).dump(); - - save_file("", "users.json", &json_str); - Ok(()) - } - - pub async fn load_users() -> io::Result<()> { - let content = load_file("", "users.json"); - if content.trim().is_empty() { - return Ok(()); - } - - let 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() { - if let Some(up) = UserProfile::from_json(j).await { - users.push(up); - } + for j in arr.iter() { + if let Some(up) = UserProfile::from_json(j).await { + users.push(up); } } - if *UNIQUE.lock().unwrap() { - Self::save_users().ok(); - } - Ok(()) } - - pub fn set_unique(val: bool) { - *UNIQUE.lock().unwrap() = val; + if *UNIQUE.lock().unwrap() { + save_users().ok(); } + Ok(()) +} + +pub fn set_unique(val: bool) { + *UNIQUE.lock().unwrap() = val; } diff --git a/src/users/user_profile.rs b/src/users/user_profile.rs index f290fdc..12530d1 100644 --- a/src/users/user_profile.rs +++ b/src/users/user_profile.rs @@ -1,15 +1,10 @@ use crate::auth::auth_connector; use crate::gui::log_panel::log_message; -use crate::users::user_manager::UserManager; +use crate::users::user_manager; use base64::{Engine as _, engine::general_purpose}; -use json::{JsonValue, object, stringify}; +use json::{JsonValue, object}; use rand::Rng; use rand::rngs::OsRng; -use std::collections::HashMap; -use std::fs; -use std::io; -use std::path::Path; -use std::sync::Mutex; use uuid::Uuid; // --- UserProfile --- @@ -81,7 +76,7 @@ impl UserProfile { { if auth_connector::migrate_user(&mut up).await { log_message(format!("[INFO] Migration triggered for {}", up.username)); - UserManager::set_unique(true); + user_manager::set_unique(true); } } diff --git a/src/util/chat_files.rs b/src/util/chat_files.rs index 4adaa60..be47cca 100644 --- a/src/util/chat_files.rs +++ b/src/util/chat_files.rs @@ -1,14 +1,10 @@ use crate::util::file_util::{get_children, load_file, save_file}; use json::{self, JsonValue, array, object}; -use sha2::digest::typenum::Add1; -use std::fs::{self, File}; -use std::io::{Read, Write}; +use std::fs::{self}; use std::path::Path; -use sys_info::loadavg; use uuid::Uuid; use crate::gui::log_panel::log_message; -use crate::langu::language_manager::format; pub enum MessageState { Read, @@ -28,172 +24,168 @@ impl MessageState { } } -pub struct ChatFiles; +pub fn add_message( + send_time: u128, + storage_owner_is_sender: bool, + storage_owner: Uuid, + external_user: Uuid, + message: &str, +) { + let user_dir = format!("users/{}/chats/{}", storage_owner, external_user); -impl ChatFiles { - pub fn add_message( - send_time: i64, - storage_owner_is_sender: bool, - storage_owner: Uuid, - external_user: Uuid, - message: &str, - ) { - let user_dir = format!("users/{}/chats/{}", storage_owner, external_user); + if let Err(e) = fs::create_dir_all(&user_dir) { + log_message(format!("Failed to create chat directory: {}", e)); + return; + } - if let Err(e) = fs::create_dir_all(&user_dir) { - log_message(format!("Failed to create chat directory: {}", e)); - return; - } + let mut chunk_index = 0; + let mut message_chunk = array![]; - 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); - // 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_message(format!("Failed to parse existing JSON file: {}", 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 { - // New file, use empty array - break; - } - - chunk_index += 1; - if chunk_index > 1000 { - log_message(format!("Too many message chunks. Aborting add.")); - return; + log_message(format!("Failed to parse existing JSON file: {}", file_name)); } + } else { + // New file, use empty array + break; } - let json_obj = object! { - "message_time" => send_time, - "message_content" => message, - "sender_is_me" => storage_owner_is_sender, - "message_state" => MessageState::Sending.as_str() - }; - - if let Err(e) = message_chunk.push(json_obj) { - log_message(format!("Failed to push new message into JSON array: {}", e)); + chunk_index += 1; + if chunk_index > 1000 { + log_message(format!("Too many message chunks. Aborting add.")); return; } - - let file_name = format!("msgs_{}.json", chunk_index); - log_message(format!("Saving message to {}/{}", user_dir, file_name)); - save_file(&user_dir, &file_name, &message_chunk.dump()); - } - pub fn change_message_state( - storage_owner: Uuid, - external_user: Uuid, - timestamp: i64, - new_state: MessageState, - ) -> std::io::Result<()> { - let user_dir = format!("users/{}/chats/{}", storage_owner, external_user); - let path = Path::new(&user_dir); - - if !path.exists() { - return Ok(()); - } - - let entries = fs::read_dir(path)?; - for entry in entries { - let entry = entry?; - let fname = entry.file_name(); - let fname_str = fname.to_string_lossy(); - - if fname_str.starts_with("msgs_") && fname_str.ends_with(".json") { - let file_content = load_file(&user_dir, &fname_str); - if file_content.is_empty() { - continue; - } - - if let Ok(mut chunk) = json::parse(&file_content) { - let mut modified = false; - for i in 0..chunk.len() { - if chunk[i]["message_time"].as_i64() == Some(timestamp) { - chunk[i]["message_state"] = JsonValue::from(new_state.as_str()); - modified = true; - break; - } - } - - if modified { - save_file(&user_dir, &fname_str, &chunk.dump()); - break; - } - } - } - } - Ok(()) } - pub fn get_messages( - storage_owner: Uuid, - external_user: Uuid, - loaded_messages: i64, - amount: i64, - ) -> JsonValue { - let mut messages = array![]; + let json_obj = object! { + "timestamp" => send_time as i64, + "content" => message, + "sent_by_self" => storage_owner_is_sender, + "message_state" => MessageState::Sending.as_str() + }; - let mut latest_chunk_index: i32 = -1; - let files = get_children(&format!("users/{}/chats/{}", storage_owner, external_user)); + if let Err(e) = message_chunk.push(json_obj) { + log_message(format!("Failed to push new message into JSON array: {}", e)); + return; + } - for entry in files { - if let Some(num) = { - entry - .strip_prefix("msgs_") - .and_then(|s| s.strip_suffix(".json")) - } { - if let Ok(index) = num.parse::() { - if index > latest_chunk_index { - latest_chunk_index = index; - } - } - } - } + let file_name = format!("msgs_{}.json", chunk_index); + log_message(format!("Saving message to {}/{}", user_dir, file_name)); + save_file(&user_dir, &file_name, &message_chunk.dump()); +} +pub fn change_message_state( + storage_owner: Uuid, + external_user: Uuid, + timestamp: i64, + new_state: MessageState, +) -> std::io::Result<()> { + let user_dir = format!("users/{}/chats/{}", storage_owner, external_user); + let path = Path::new(&user_dir); - if latest_chunk_index == -1 { - return messages; - } + if !path.exists() { + return Ok(()); + } - let mut to_skip = loaded_messages; - let mut needed = amount; + let entries = fs::read_dir(path)?; + for entry in entries { + let entry = entry?; + let fname = entry.file_name(); + let fname_str = fname.to_string_lossy(); - for chunk_index in (0..=latest_chunk_index).rev() { - if needed == 0 { - break; - } - let file_name = format!("msgs_{}.json", chunk_index); - let file_content = load_file( - &format!("users/{}/chats/{}", storage_owner, external_user), - &file_name, - ); + if fname_str.starts_with("msgs_") && fname_str.ends_with(".json") { + let file_content = load_file(&user_dir, &fname_str); if file_content.is_empty() { continue; } - if let Ok(chunk) = json::parse(&file_content) { - for i in (0..chunk.len()).rev() { - if needed == 0 { + + if let Ok(mut chunk) = json::parse(&file_content) { + let mut modified = false; + for i in 0..chunk.len() { + if chunk[i]["message_time"].as_i64() == Some(timestamp) { + chunk[i]["message_state"] = JsonValue::from(new_state.as_str()); + modified = true; break; } - if to_skip > 0 { - to_skip -= 1; - continue; - } - messages.push(chunk[i].clone()).unwrap(); - needed -= 1; + } + + if modified { + save_file(&user_dir, &fname_str, &chunk.dump()); + break; } } } - - messages } + Ok(()) +} + +pub fn get_messages( + storage_owner: Uuid, + external_user: Uuid, + loaded_messages: i64, + amount: i64, +) -> JsonValue { + let mut messages = array![]; + + let mut latest_chunk_index: i32 = -1; + let files = get_children(&format!("users/{}/chats/{}", storage_owner, external_user)); + + for entry in files { + if let Some(num) = { + entry + .strip_prefix("msgs_") + .and_then(|s| s.strip_suffix(".json")) + } { + if let Ok(index) = num.parse::() { + if index > latest_chunk_index { + latest_chunk_index = index; + } + } + } + } + + if latest_chunk_index == -1 { + return messages; + } + + let mut to_skip = loaded_messages; + let mut needed = amount; + + for chunk_index in (0..=latest_chunk_index).rev() { + if needed == 0 { + break; + } + let file_name = format!("msgs_{}.json", chunk_index); + let file_content = load_file( + &format!("users/{}/chats/{}", storage_owner, external_user), + &file_name, + ); + if file_content.is_empty() { + continue; + } + if let Ok(chunk) = json::parse(&file_content) { + for i in (0..chunk.len()).rev() { + if needed == 0 { + break; + } + if to_skip > 0 { + to_skip -= 1; + continue; + } + messages.push(chunk[i].clone()).unwrap(); + needed -= 1; + } + } + } + + messages } diff --git a/src/util/chats_util.rs b/src/util/chats_util.rs index 618b7a2..b824f13 100644 --- a/src/util/chats_util.rs +++ b/src/util/chats_util.rs @@ -1,7 +1,4 @@ -use axum::Json; use json::{self, JsonValue, array}; -use std::path::Path; -use std::string::String; use uuid::Uuid; use crate::users::contact::Contact; diff --git a/src/util/config_util.rs b/src/util/config_util.rs index 08bbde6..6659598 100644 --- a/src/util/config_util.rs +++ b/src/util/config_util.rs @@ -1,8 +1,6 @@ use crate::util::file_util::{load_file, save_file}; use json::JsonValue; use once_cell::sync::Lazy; -use std::fs::{self, File}; -use std::path::Path; use std::sync::Mutex; use uuid::Uuid; @@ -36,6 +34,10 @@ impl ConfigUtil { .unwrap_or_default() } + pub fn get_port(&self) -> u16 { + self.config["port"].as_u16().unwrap_or(1984) + } + pub fn change(&mut self, key: &str, value: Uuid) { self.config[key] = JsonValue::String(value.to_string()); self.unique = true; diff --git a/src/util/file_util.rs b/src/util/file_util.rs index 76e873f..774a5e3 100644 --- a/src/util/file_util.rs +++ b/src/util/file_util.rs @@ -1,10 +1,7 @@ use std::ffi::OsStr; -use std::fmt::Write as FmtWrite; use std::fs::{self, File}; -use std::io::{Read, Write}; +use std::io::Read; use std::path::{Path, PathBuf}; -use std::process; -use std::time::SystemTime; use sysinfo::System; use uuid::Uuid; use walkdir::WalkDir;