This commit is contained in:
Alois 2026-04-02 22:25:20 +02:00
commit 32d44eb425
88 changed files with 4756 additions and 12014 deletions

View file

@ -0,0 +1,519 @@
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue};
use ttp_native::{Receiver, Sender};
use uuid::Uuid;
use crate::anonymous_clients::anonymous_manager::{self, generate_username};
use crate::calls::call_manager;
use crate::omega::omega_connection::get_omega_connection;
use crate::rho::connection::GeneralConnection;
use crate::rho::rho_manager;
use crate::util::logger::PrintType;
use crate::{log_cv_in, log_cv_out, log_out};
pub struct AnonymousClientConnection {
user_id: u64,
pub sender: Arc<Sender>,
pub receiver: Arc<Receiver>,
pub ping: Arc<RwLock<i64>>,
pub interested_users: Arc<RwLock<Vec<i64>>>,
is_open: Arc<RwLock<bool>>,
pub user_name: Arc<RwLock<String>>,
pub display_name: Arc<RwLock<String>>,
pub avatar: Arc<RwLock<String>>,
}
impl AnonymousClientConnection {
pub async fn from_general(general: Arc<GeneralConnection>, user_id: u64) -> Arc<Self> {
let username: String = generate_username();
Arc::new(Self {
user_id: user_id,
ping: Arc::new(RwLock::new(0)),
interested_users: Arc::new(RwLock::new(Vec::new())),
is_open: Arc::new(RwLock::new(true)),
sender: general.sender.clone(),
receiver: general.receiver.clone(),
user_name: Arc::new(RwLock::new(username.to_lowercase())),
display_name: Arc::new(RwLock::new(username)),
avatar: Arc::new(RwLock::new(String::new())),
})
}
pub fn start(self: Arc<Self>) {
let self_clone = self.clone();
tokio::spawn(async move {
while let Ok(cv) = self_clone.receiver.receive().await {
self_clone.clone().handle_message(cv).await;
}
self_clone.handle_close().await;
});
}
/// Get the user ID
pub fn get_user_id(&self) -> u64 {
self.user_id
}
/// Get the user name
pub async fn get_user_name(&self) -> String {
self.user_name.read().await.clone()
}
/// Get the display name
pub async fn get_display_name(&self) -> String {
self.display_name.read().await.clone()
}
pub async fn set_display_name(&self, display: String) {
*self.display_name.write().await = display;
}
/// Get the avatar
pub async fn get_avatar(&self) -> String {
self.avatar.read().await.clone()
}
/// Send a CommunicationValue to the client
pub async fn send_message(self: Arc<Self>, cv: &CommunicationValue) {
if !*self.is_open.read().await {
log_out!(
self.user_id as i64,
PrintType::Client,
"Attempted to send message to a closed connection."
);
return;
}
if !cv.is_type(CommunicationType::pong) {
log_cv_out!(PrintType::Client, &cv);
}
if let Err(e) = self.sender.send(&cv).await {
log_out!(
self.user_id as i64,
PrintType::Client,
"Send failed: {:?}",
e
);
}
}
/// Handle incoming message from client
pub async fn handle_message(self: Arc<Self>, cv: CommunicationValue) {
tokio::spawn(async move {
if cv.is_type(CommunicationType::ping) {
self.handle_ping(cv).await;
return;
}
log_cv_in!(PrintType::Client, &cv);
if cv.is_type(CommunicationType::identification) {
let call_id =
Uuid::parse_str(cv.get_data(DataTypes::call_id).as_str().unwrap_or(""))
.unwrap_or(Uuid::new_v4());
let call = if let Some(call) = call_manager::get_call(call_id).await {
if call.is_anonymous().await {
call
} else {
self.send_error_response(
&cv.get_id(),
CommunicationType::error_not_authenticated,
)
.await;
return;
}
} else {
self.send_error_response(
&cv.get_id(),
CommunicationType::error_not_authenticated,
)
.await;
return;
};
let mut invited = Vec::new();
for call_invitee in call.members.read().await.clone() {
let call_invitee_cv = get_omega_connection()
.await_response(
&CommunicationValue::new(CommunicationType::get_user_data).add_data(
DataTypes::user_id,
DataValue::Number(call_invitee.user_id as i64),
),
Some(Duration::from_secs(2)),
)
.await
.unwrap();
let mut json_invitee = Vec::new();
let _ = json_invitee.push((
DataTypes::user_id,
call_invitee_cv.get_data(DataTypes::user_id).clone(),
));
let _ = json_invitee.push((
DataTypes::username,
call_invitee_cv.get_data(DataTypes::username).clone(),
));
let _ = json_invitee.push((
DataTypes::display,
call_invitee_cv.get_data(DataTypes::display).clone(),
));
let _ = json_invitee.push((
DataTypes::avatar,
call_invitee_cv.get_data(DataTypes::avatar).clone(),
));
let _ = invited.push(DataValue::Container(json_invitee));
}
let token = call.create_anonymous_token(self.get_user_id()).await;
let mut serialized = Vec::new();
let _ = serialized.push((DataTypes::call_id, DataValue::Str(call_id.to_string())));
let _ =
serialized.push((DataTypes::call_invited, DataValue::Array(invited.clone())));
let _ = serialized.push((DataTypes::call_members, DataValue::Array(invited)));
let _ = serialized.push((DataTypes::call_token, DataValue::Str(token.unwrap())));
self.clone()
.send_message(
&&CommunicationValue::new(CommunicationType::identification_response)
.with_id(cv.get_id())
.add_data(DataTypes::user_id, DataValue::Number(self.user_id as i64))
.add_data(
DataTypes::username,
DataValue::Str(self.clone().get_user_name().await),
)
.add_data(
DataTypes::display,
DataValue::Str(self.get_display_name().await),
)
.add_data(DataTypes::avatar, DataValue::Str(self.get_avatar().await))
.add_data(DataTypes::call_state, DataValue::Container(serialized)),
)
.await;
}
// Handle ping
if cv.is_type(CommunicationType::ping) {
self.handle_ping(cv).await;
return;
}
// Handle client status changes
if cv.is_type(CommunicationType::client_changed) {
self.handle_client_changed(cv).await;
return;
}
// Handle call invites
if cv.is_type(CommunicationType::call_invite) {
self.handle_call_invite(cv).await;
return;
}
// Handle get call requests
if cv.is_type(CommunicationType::call_token) {
self.handle_get_call(cv).await;
return;
}
if cv.is_type(CommunicationType::call_disconnect_user) {
self.handle_call_disconnect_user(cv).await;
return;
}
if cv.is_type(CommunicationType::call_timeout_user) {
self.handle_call_timeout_user(cv).await;
return;
}
if cv.is_type(CommunicationType::change_user_data) {
if let Some(display_name) = cv.get_data(DataTypes::display).as_str() {
let _ = self.set_display_name(display_name.to_string()).await;
}
return;
}
if cv.is_type(CommunicationType::get_user_data) {
if let Some(anonymous) = {
if let Some(user_id) = cv.get_data(DataTypes::user_id).as_number() {
anonymous_manager::get_anonymous_user(user_id as u64).await
} else if let Some(username) = cv.get_data(DataTypes::username).as_str() {
anonymous_manager::get_anonymous_user_by_name(username.to_string()).await
} else {
None
}
} {
let response = CommunicationValue::new(CommunicationType::get_user_data)
.with_id(cv.get_id())
.add_data(
DataTypes::username,
DataValue::Str(anonymous.get_user_name().await),
)
.add_data(
DataTypes::user_id,
DataValue::Number(anonymous.user_id as i64),
)
.add_data(
DataTypes::display,
DataValue::Str(anonymous.get_display_name().await),
)
.add_data(DataTypes::user_state, DataValue::Str("online".to_string()))
.add_data(
DataTypes::avatar,
DataValue::Str(anonymous.get_avatar().await),
);
self.send_message(&response).await;
return;
}
}
if cv.is_type(CommunicationType::get_user_data)
|| cv.is_type(CommunicationType::get_iota_data)
|| cv.is_type(CommunicationType::delete_user)
{
self.handle_omega_forward(cv).await;
return;
}
});
}
async fn handle_omega_forward(self: Arc<Self>, cv: CommunicationValue) {
let client_for_closure = self.clone();
tokio::spawn(async move {
let response_cv = get_omega_connection()
.await_response(&cv.with_sender(self.user_id), Some(Duration::from_secs(20)))
.await;
if let Ok(response_cv) = response_cv {
client_for_closure.send_message(&response_cv).await;
}
});
}
/// Handle ping message
async fn handle_ping(self: Arc<Self>, cv: CommunicationValue) {
// Update our ping if provided
if let DataValue::Number(last_ping) = cv.get_data(DataTypes::last_ping) {
if let Ok(ping_val) = last_ping.to_string().parse::<i64>() {
let mut ping_guard = self.ping.write().await;
*ping_guard = ping_val;
}
}
// Send pong response
let response = CommunicationValue::new(CommunicationType::pong).with_id(cv.get_id());
self.send_message(&response).await;
}
/// Handle client status change
async fn handle_client_changed(self: Arc<Self>, _cv: CommunicationValue) {
/*let user_id = self.get_user_id().await;
if let Some(_status_str) = cv.get_data(DataTypes::user_state) {
let user_status = UserStatus::online;
}*/
}
/// Handle call invite
async fn handle_call_invite(self: Arc<Self>, cv: CommunicationValue) {
let receiver_id: i64 = cv.get_data(DataTypes::receiver_id).as_number().unwrap_or(0);
if receiver_id == 0 {
self.send_error_response(&cv.get_id(), CommunicationType::error_no_user_id)
.await;
return;
}
let call_id = match cv.get_data(DataTypes::call_id) {
DataValue::Str(id_str) => match Uuid::parse_str(&id_str.to_string()) {
Ok(id) => id,
Err(_) => {
self.send_error_response(
&cv.get_id(),
CommunicationType::error_invalid_call_id,
)
.await;
return;
}
},
_ => {
self.send_error_response(&cv.get_id(), CommunicationType::error_no_call_id)
.await;
return;
}
};
let invited = call_manager::add_invite(call_id, self.user_id, receiver_id as u64).await;
if !invited {
self.send_error_response(&cv.get_id(), CommunicationType::error_invalid_call_id)
.await;
return;
}
// Find target RhoConnection
let target_rho = match rho_manager::get_rho_con_for_user(receiver_id).await {
Some(rho) => rho,
_ => {
self.send_error_response(&cv.get_id(), CommunicationType::error)
.await;
return;
}
};
// Get sender user ID
let sender_id = self.get_user_id();
// Create and send call distribution message
let forward = CommunicationValue::new(CommunicationType::call_invite)
.with_receiver(receiver_id as u64)
.with_sender(sender_id)
.add_data(DataTypes::call_id, DataValue::Str(call_id.to_string()))
.add_data(
DataTypes::receiver_id,
DataValue::Str(receiver_id.to_string()),
)
.add_data(DataTypes::sender_id, DataValue::Str(sender_id.to_string()));
target_rho.message_to_client(forward).await;
let response = CommunicationValue::new(CommunicationType::success).with_id(cv.get_id());
self.send_message(&response).await;
}
/// Handle get call request
async fn handle_get_call(self: Arc<Self>, cv: CommunicationValue) {
let user_id = self.get_user_id();
let call_id = match cv.get_data(DataTypes::call_id) {
DataValue::Str(id_str) => match Uuid::parse_str(&id_str.to_string()) {
Ok(id) => id,
Err(_) => {
self.send_error_response(&cv.get_id(), CommunicationType::error)
.await;
return;
}
},
_ => {
self.send_error_response(&cv.get_id(), CommunicationType::error)
.await;
return;
}
};
if let Some(token) = call_manager::get_call_token(user_id, call_id).await {
let response = CommunicationValue::new(CommunicationType::call_token)
.with_id(cv.get_id())
.with_receiver(user_id)
.add_data(DataTypes::call_token, DataValue::Str(token.to_string()));
self.send_message(&response).await;
} else {
self.send_error_response(&cv.get_id(), CommunicationType::error)
.await;
return;
}
}
async fn handle_call_timeout_user(self: Arc<Self>, cv: CommunicationValue) {
let call_id =
Uuid::from_str(cv.get_data(DataTypes::call_id).as_str().unwrap_or("")).unwrap();
let user_id = cv.get_data(DataTypes::user_id).as_number().unwrap_or(0);
let untill = cv.get_data(DataTypes::untill).as_number().unwrap_or(0);
let call = call_manager::get_call(call_id).await;
if let Some(call) = call {
if call
.get_caller(self.get_user_id())
.await
.unwrap()
.has_admin()
{
call.get_caller(user_id as u64)
.await
.unwrap()
.set_timeout(untill)
.await;
}
}
}
async fn handle_call_disconnect_user(self: Arc<Self>, cv: CommunicationValue) {
let call_id =
Uuid::from_str(cv.get_data(DataTypes::call_id).as_str().unwrap_or("")).unwrap();
let user_id = cv.get_data(DataTypes::user_id).as_number().unwrap_or(0);
let call = call_manager::get_call(call_id).await;
if let Some(call) = call {
if call
.get_caller(self.get_user_id())
.await
.unwrap()
.has_admin()
{
call.remove_caller(user_id as u64).await;
}
}
}
/// Send error response
async fn send_error_response(self: Arc<Self>, message_id: &u32, error_type: CommunicationType) {
let error = CommunicationValue::new(error_type).with_id(*message_id);
self.send_message(&error).await;
}
/// Close the connection
pub async fn close(&self) {
let mut is_open_guard = self.is_open.write().await;
if !*is_open_guard {
return;
}
*is_open_guard = false;
let _ = self.sender.close();
}
#[allow(dead_code)]
/// Set interested users list
pub async fn set_interested_users(self: Arc<Self>, interested_ids: Vec<i64>) {
let mut interested_guard = self.interested_users.write().await;
*interested_guard = interested_ids;
}
#[allow(dead_code)]
pub async fn get_interested_users(self: Arc<Self>) -> Vec<i64> {
let interested_guard = self.interested_users.read().await;
interested_guard.clone()
}
#[allow(dead_code)]
/// Check if interested in a user and send notification
pub async fn are_you_interested(self: Arc<Self>, user_id: i64) {
let interested_guard = self.clone().get_interested_users().await;
if interested_guard.contains(&user_id) {
let notification = CommunicationValue::new(CommunicationType::client_changed)
.add_data(DataTypes::user_id, DataValue::Str(user_id.to_string()))
.add_data(DataTypes::user_state, DataValue::Str("online".to_string()));
self.send_message(&notification).await;
}
}
/// Handle connection close
pub async fn handle_close(&self) {
// TODO delete temp user
}
}
// Implement Clone to make it easier to work with Arc<AnonymousClientConnection>
impl Clone for AnonymousClientConnection {
fn clone(&self) -> Self {
Self {
sender: Arc::clone(&self.sender),
receiver: Arc::clone(&self.receiver),
user_id: self.user_id,
ping: Arc::clone(&self.ping),
interested_users: Arc::clone(&self.interested_users),
is_open: Arc::clone(&self.is_open),
user_name: Arc::clone(&self.user_name),
display_name: Arc::clone(&self.display_name),
avatar: Arc::clone(&self.avatar),
}
}
}

View file

@ -0,0 +1,53 @@
use dashmap::DashMap;
use once_cell::sync::Lazy;
use rand::Rng;
use rand::seq::SliceRandom;
use std::sync::Arc;
use crate::anonymous_clients::anonymous_client_connection::AnonymousClientConnection;
static ANONYMOUS_USERS: Lazy<DashMap<u64, Arc<AnonymousClientConnection>>> =
Lazy::new(|| DashMap::new());
#[allow(dead_code)]
pub async fn add_anonymous_user(connection: Arc<AnonymousClientConnection>) {
ANONYMOUS_USERS.insert(connection.get_user_id(), connection);
}
#[allow(dead_code)]
pub async fn remove_anonymous_user(user_id: u64) {
ANONYMOUS_USERS.remove(&user_id);
}
pub async fn get_anonymous_user(user_id: u64) -> Option<Arc<AnonymousClientConnection>> {
ANONYMOUS_USERS.get(&user_id).map(|c| c.clone())
}
pub async fn get_anonymous_user_by_name(
username: String,
) -> Option<Arc<AnonymousClientConnection>> {
for user_conn in ANONYMOUS_USERS
.iter()
.map(|ref_multi| ref_multi.value().clone())
{
if user_conn.get_user_name().await == username {
return Some(user_conn);
}
}
return None;
}
// TODO: implement check if taken
pub fn generate_username() -> String {
let adjectives = ["Swift", "Clever", "Brave", "Sneaky", "Fierce"];
let nouns = ["Tiger", "Eagle", "Shark", "Wolf", "Dragon"];
let mut rng = rand::thread_rng();
let adj = adjectives.choose(&mut rng).unwrap();
let noun = nouns.choose(&mut rng).unwrap();
let number: u16 = rng.gen_range(0..10000);
format!("{}{}{}", adj, noun, number)
}

View file

@ -0,0 +1,2 @@
pub mod anonymous_client_connection;
pub mod anonymous_manager;

View file

@ -1,18 +0,0 @@
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct AuthUser {
pub id: i64,
pub username: String,
pub password_hash: String,
pub public_key: String,
}
impl AuthUser {
pub fn new(id: i64, username: String, password_hash: String, public_key: String) -> Self {
AuthUser {
id,
username,
password_hash,
public_key,
}
}
}

View file

@ -1,18 +0,0 @@
use json::JsonValue;
use crate::util::file_util::load_file;
// NOT USED AT MOMENT
pub fn is_private_key_valid(user_id: &i64, key_hash: &str) -> bool {
let file_contents = load_file("", "users.json");
let users = json::parse(&file_contents).unwrap();
if let JsonValue::Array(users_array) = users {
for user in users_array {
if user["uuid"] == user_id.to_string() && user["private_key_hash"] == key_hash {
return true;
}
}
}
false
}

View file

@ -1,2 +0,0 @@
pub mod auth_user;
pub mod local_auth;

106
src/calls/call_group.rs Normal file
View file

@ -0,0 +1,106 @@
use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue};
use std::{env, sync::Arc, time::Duration};
use tokio::sync::RwLock;
use uuid::Uuid;
use crate::{
calls::{call_util, caller::Caller},
omega::omega_connection::get_omega_connection,
};
pub struct CallGroup {
pub call_id: Uuid,
pub members: RwLock<Vec<Arc<Caller>>>,
pub show: RwLock<bool>,
pub anonymous_joining: RwLock<bool>,
pub short_link: RwLock<Option<String>>,
}
impl CallGroup {
pub fn new(call_id: Uuid, user: Arc<Caller>) -> Self {
CallGroup {
call_id,
members: RwLock::new(vec![user]),
show: RwLock::new(true),
anonymous_joining: RwLock::new(false),
short_link: RwLock::new(None),
}
}
pub async fn get_caller(&self, user_id: u64) -> Option<Arc<Caller>> {
self.members
.read()
.await
.iter()
.find(|caller| caller.user_id == user_id)
.cloned()
}
pub async fn is_anonymous(&self) -> bool {
*self.anonymous_joining.read().await
}
pub async fn set_anonymous_joining(&self, enable: bool) {
*self.anonymous_joining.write().await = enable;
let _ = call_util::set_room_metadata(
self.call_id,
format!("{{\"anonymous_joining\": \"{}\"}}", enable),
)
.await;
if self.short_link.read().await.is_none() {
let long_link = format!(
"https://app.tensamin.net/call/anonymous?call_id={}&omikron_id={}",
self.call_id,
env::var("ID")
.unwrap_or("0".to_string())
.parse::<i64>()
.unwrap_or(0),
);
let response_cv = get_omega_connection()
.await_response(
&CommunicationValue::new(CommunicationType::shorten_link)
.add_data(DataTypes::link, DataValue::Str(long_link)),
Some(Duration::from_secs(20)),
)
.await;
if let Ok(response) = response_cv {
*self.short_link.write().await = Some(
response
.get_data(DataTypes::link)
.as_str()
.unwrap()
.to_string(),
);
log::info!(
"Shortened link for call {} is {}",
self.call_id,
self.short_link.read().await.as_ref().unwrap()
);
}
}
}
pub async fn create_anonymous_token(&self, user_id: u64) -> Option<String> {
if self.is_anonymous().await {
if let Ok(token) = call_util::create_token(user_id, self.call_id, false) {
return Some(token);
}
}
None
}
pub async fn remove_caller(&self, user_id: u64) {
let _ = call_util::remove_participant(self.call_id, user_id).await;
self.members
.write()
.await
.retain(|caller| caller.user_id != user_id);
}
pub async fn get_short_link(self: Arc<Self>) -> Option<String> {
self.short_link.read().await.clone()
}
}

97
src/calls/call_manager.rs Normal file
View file

@ -0,0 +1,97 @@
use dashmap::DashMap;
use once_cell::sync::Lazy;
use std::sync::Arc;
use uuid::Uuid;
use crate::calls::{call_group::CallGroup, caller::Caller};
pub static CALL_GROUPS: Lazy<DashMap<Uuid, Arc<CallGroup>>> = Lazy::new(|| DashMap::new());
#[allow(dead_code)]
pub async fn get_call_invites(user_id: u64) -> Vec<Arc<Caller>> {
let mut callers = Vec::new();
for (_, cg) in CALL_GROUPS.clone().into_iter() {
let members = cg.members.read().await;
for member in members.iter() {
if member.user_id == user_id {
callers.push(member.clone());
}
}
}
callers
}
pub async fn get_call(call_id: Uuid) -> Option<Arc<CallGroup>> {
if let Some(b) = CALL_GROUPS.get(&call_id) {
Some(b.clone())
} else {
None
}
}
pub async fn get_call_groups(user_id: u64) -> Vec<Arc<CallGroup>> {
let mut call_groups = Vec::new();
for (_, cg) in CALL_GROUPS.clone().into_iter() {
let is_member = {
let members = cg.members.read().await;
members.iter().any(|m| m.user_id == user_id)
};
if is_member {
call_groups.push(cg.clone());
}
}
call_groups
}
pub async fn get_call_token(user_id: u64, call_id: Uuid) -> Option<String> {
if let Some(cg) = CALL_GROUPS.get(&call_id) {
let mut members = cg.members.write().await;
if let Some(member) = members.iter().find(|m| m.user_id == user_id) {
return Some(member.create_token());
}
let new_caller = Arc::new(Caller::new(user_id, call_id, false));
let token = new_caller.create_token();
members.push(new_caller);
return Some(token);
}
if let Some(cg) = CALL_GROUPS.get(&call_id) {
let cg_clone = cg.clone();
let mut members = cg_clone.members.write().await;
if let Some(member) = members.iter().find(|m| m.user_id == user_id) {
return Some(member.create_token());
}
let new_caller = Arc::new(Caller::new(user_id, call_id, false));
let token = new_caller.create_token();
members.push(new_caller);
return Some(token);
}
let caller = Arc::new(Caller::new(user_id, call_id, true));
let call_group = CallGroup::new(call_id, caller.clone());
CALL_GROUPS.insert(call_id, Arc::new(call_group));
Some(caller.create_token())
}
pub async fn add_invite(call_id: Uuid, inviter_id: u64, invitee_id: u64) -> bool {
if let Some(cg) = CALL_GROUPS.get(&call_id) {
let mut members = cg.members.write().await;
let is_inviter_member = members.iter().any(|m| m.user_id == inviter_id);
if is_inviter_member {
if !members.iter().any(|m| m.user_id == invitee_id) {
members.push(Arc::new(Caller::new(invitee_id, call_id, false)));
}
return true;
}
}
false
}

151
src/calls/call_util.rs Normal file
View file

@ -0,0 +1,151 @@
use livekit_api::{
access_token::{self},
services::room::RoomClient,
};
use livekit_protocol::Room;
use std::env;
use std::str::FromStr;
use std::time::Duration;
use uuid::Uuid;
use crate::{calls::call_manager::CALL_GROUPS, log, log_err, util::logger::PrintType};
pub fn get_livekit() -> Result<(String, String, String), ()> {
let hostname = match env::var("LIVEKI_HOSTNAME") {
Ok(secret) => secret,
Err(_) => {
log_err!(0, PrintType::General, "LIVEKI_HOSTNAME not set!");
return Err(());
}
};
let api_key = match env::var("LIVEKIT_API_KEY") {
Ok(key) => key,
Err(_) => {
log_err!(0, PrintType::General, "LIVEKIT_API_KEY not set!");
return Err(());
}
};
let api_secret = match env::var("LIVEKIT_API_SECRET") {
Ok(secret) => secret,
Err(_) => {
log_err!(0, PrintType::General, "LIVEKIT_API_SECRET not set!");
return Err(());
}
};
Ok((hostname, api_key, api_secret))
}
pub fn create_token(user_id: u64, call_id: Uuid, has_admin: bool) -> Result<String, ()> {
let (_, api_key, api_secret) = get_livekit()?;
let token = access_token::AccessToken::with_api_key(&api_key, &api_secret)
.with_identity(&user_id.to_string())
.with_grants(access_token::VideoGrants {
room_join: true,
room_admin: has_admin,
room: call_id.to_string(),
..Default::default()
})
.with_metadata(&format!("{{\"isAdmin\":{}}}", has_admin))
.to_jwt();
if let Ok(token) = token {
Ok(token)
} else {
Err(())
}
}
#[allow(dead_code)]
pub async fn get_room(call_id: Uuid) -> Result<(RoomClient, Room), ()> {
if let Ok((hostname, api_key, api_secret)) = get_livekit() {
let room_service = RoomClient::with_api_key(&hostname, &api_key, &api_secret);
let rooms = room_service.list_rooms(Vec::new()).await;
if let Ok(rooms) = rooms {
for room in rooms {
if room.name == call_id.to_string() {
return Ok((room_service, room));
}
}
}
}
return Err(());
}
pub async fn remove_participant(call_id: Uuid, user_id: u64) -> Result<(), ()> {
if let Ok((hostname, api_key, api_secret)) = get_livekit() {
let room_service = RoomClient::with_api_key(&hostname, &api_key, &api_secret);
if let Ok(_) = room_service
.remove_participant(&call_id.to_string(), &user_id.to_string())
.await
{
return Ok(());
}
}
return Err(());
}
#[allow(dead_code)]
pub async fn get_room_metadata(call_id: Uuid) -> Result<String, ()> {
if let Ok((_, room)) = get_room(call_id).await {
Ok(room.metadata)
} else {
Err(())
}
}
pub async fn set_room_metadata(call_id: Uuid, metadata: String) -> Result<(), ()> {
if let Ok((hostname, api_key, api_secret)) = get_livekit() {
let room_service = RoomClient::with_api_key(&hostname, &api_key, &api_secret);
if let Ok(_) = room_service
.update_room_metadata(&call_id.to_string(), &metadata)
.await
{
return Ok(());
}
}
return Err(());
}
pub fn garbage_collect_calls() {
tokio::spawn(async move {
loop {
if let Ok((hostname, api_key, api_secret)) = get_livekit() {
let room_service = RoomClient::with_api_key(&hostname, &api_key, &api_secret);
clean_calls(room_service).await;
}
tokio::time::sleep(Duration::from_secs(2)).await;
}
});
}
pub async fn clean_calls(room_service: RoomClient) {
let rooms = room_service.list_rooms(Vec::new()).await.unwrap();
let mut call_ids: Vec<Uuid> = Vec::new();
let mut no_users: Vec<Uuid> = Vec::new();
for room in rooms {
if let Ok(id) = Uuid::from_str(&room.name) {
if room.num_participants == 0 {
no_users.push(id);
}
call_ids.push(id);
}
}
let size_pre = CALL_GROUPS.len();
for (id, _) in CALL_GROUPS.clone().into_iter() {
if !call_ids.contains(&id) {
CALL_GROUPS.remove(&id);
}
}
for (_, cg) in CALL_GROUPS.clone().into_iter() {
*cg.show.write().await = !no_users.contains(&cg.call_id);
}
let size_post = CALL_GROUPS.len();
if size_pre - size_post != 0 {
log!(
0,
PrintType::Call,
"Cleaned {} calls, {} remaining",
size_pre - size_post,
size_post
);
}
}

49
src/calls/caller.rs Normal file
View file

@ -0,0 +1,49 @@
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::sync::RwLock;
use uuid::Uuid;
use crate::calls::call_util;
pub struct Caller {
pub user_id: u64,
pub call_id: Uuid,
pub has_admin: bool,
pub timeout: RwLock<i64>,
}
impl Caller {
pub fn new(user_id: u64, call_id: Uuid, has_admin: bool) -> Self {
Caller {
user_id,
call_id,
has_admin,
timeout: RwLock::new(0),
}
}
#[allow(dead_code)]
pub fn set_admin(&mut self, has_admin: bool) {
self.has_admin = has_admin;
}
pub fn has_admin(&self) -> bool {
self.has_admin
}
#[allow(dead_code)]
pub async fn is_timeouted(&self) -> bool {
*self.timeout.read().await
> SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as i64
}
pub async fn set_timeout(&self, timeout: i64) {
*self.timeout.write().await = timeout;
}
pub fn create_token(&self) -> String {
if let Ok(token) = call_util::create_token(self.user_id, self.call_id, self.has_admin()) {
token
} else {
String::new()
}
}
}

4
src/calls/mod.rs Normal file
View file

@ -0,0 +1,4 @@
pub mod call_group;
pub mod call_manager;
pub mod call_util;
pub mod caller;

View file

@ -1,356 +0,0 @@
use crate::communities::interactables::category::Category;
use crate::communities::interactables::registry;
use crate::communities::perms::permission::Permission;
use crate::communities::{
community_connection::CommunityConnection, interactables::interactable::Interactable,
};
use crate::util::file_util;
use base64::{Engine as _, engine::general_purpose::STANDARD};
use json::JsonValue;
use json::number::Number;
use json::object::Object;
use rand::RngCore;
use rand_core::OsRng;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
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: Arc<RwLock<i64>>,
members: Vec<i64>,
permissions: HashMap<i64, Vec<Permission>>,
roles: HashMap<String, Vec<Permission>>,
private_key: Secret,
public_key: PublicKey,
pub interactables: Arc<RwLock<Vec<Arc<Box<dyn Interactable>>>>>,
pub connections: Arc<RwLock<HashMap<i64, Vec<Arc<CommunityConnection>>>>>,
}
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: Arc::new(RwLock::new(0)),
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 async fn create(name: String, owner_id: i64) -> 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);
let c = Community {
name,
owner_id: Arc::new(RwLock::new(owner_id)),
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())),
};
c.save().await;
c
}
pub async fn to_json(&self) -> JsonValue {
let mut json = JsonValue::new_object();
json["name"] = self.name.clone().into();
json["owner_id"] = (*self.owner_id.read().await as i64).into();
json["members"] = self
.members
.clone()
.iter()
.map(|f| f.to_string())
.collect::<Vec<String>>()
.into();
json["private_key"] = self.private_key.as_bytes().to_vec().into();
json["public_key"] = self.public_key.as_bytes().to_vec().into();
json["connections"] = self.connections.read().await.clone().len().into();
json
}
pub async fn frontend(&self) -> JsonValue {
let mut json = JsonValue::new_object();
json["name"] = self.name.clone().into();
json["owner_id"] = (*self.owner_id.read().await as i64).into();
json["members"] = self
.members
.clone()
.iter()
.map(|f| f.to_string())
.collect::<Vec<String>>()
.into();
json["public_key"] = self.public_key.as_bytes().to_vec().into();
json["connections"] = self.connections.read().await.clone().len().into();
json
}
pub fn add_member(&mut self, member_id: i64) {
self.members.push(member_id);
}
pub fn remove_member(&mut self, member_id: i64) {
self.members.retain(|id| *id != member_id);
}
pub fn get_name(&self) -> &str {
&self.name
}
pub async fn get_owner_id(&self) -> i64 {
*self.owner_id.read().await
}
pub fn get_members(&self) -> Vec<i64> {
self.members.clone()
}
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 set_owner(self: Arc<Community>, owner_id: i64) {
*self.owner_id.write().await = owner_id;
}
pub async fn add_connection(self: &Arc<Self>, other: Arc<CommunityConnection>) {
let mut vec = self
.connections
.read()
.await
.get(&other.get_user_id().await)
.cloned()
.unwrap_or_default();
vec.push(other.clone());
self.connections
.write()
.await
.insert(other.get_user_id().await, vec);
}
pub async fn remove_connection(self: &Arc<Self>, other: Arc<CommunityConnection>) {
let mut vec = self
.connections
.read()
.await
.get(&other.get_user_id().await)
.cloned()
.unwrap_or_default();
vec.retain(|conn| !Arc::ptr_eq(conn, &other));
self.connections
.write()
.await
.insert(other.get_user_id().await, vec);
}
pub async fn get_connections(&self) -> HashMap<i64, Vec<Arc<CommunityConnection>>> {
self.connections.read().await.clone()
}
pub async fn get_connections_for_user(&self, user_id: i64) -> Vec<Arc<CommunityConnection>> {
self.connections
.read()
.await
.get(&user_id)
.cloned()
.unwrap_or_default()
}
pub async fn get_interactables(
&self,
_user_id: i64,
) -> Vec<Arc<Box<dyn Interactable + 'static>>> {
self.interactables.read().await.clone()
}
pub async fn add_interactable(self: &mut Arc<Self>, interactable: Arc<Box<dyn Interactable>>) {
self.interactables.write().await.push(interactable);
}
pub async fn remove_interactable(
self: &mut Arc<Self>,
interactable: Arc<Box<dyn Interactable>>,
) {
self.interactables
.write()
.await
.retain(|i| !Arc::ptr_eq(i, &interactable));
}
pub async fn broadcast_message(&self, message: &CommunicationValue) {
for (_, conns) in self.connections.read().await.clone().iter() {
for user in conns.iter() {
user.send_message(message).await;
}
}
}
pub async fn run_function(
self: &mut Arc<Self>,
_user_id: i64,
name: &str,
path: &str,
_function: &str,
cv: &CommunicationValue,
) -> CommunicationValue {
if path.is_empty() {
let 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 {
// cannot move a value of type dyn Interactable the size of dyn Interactable cannot be statically determined (rustc E0161)
return interactable.run_function(cv.clone()).await;
}
}
}
} 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::<Category>().unwrap();
// cannot move a value of type dyn Interactable the size of dyn Interactable cannot be statically determined (rustc E0161)
return category
.get_child(path.to_string(), name.to_string())
.unwrap()
.run_function(cv.clone())
.await;
} else {
return CommunicationValue::new(CommunicationType::error);
}
}
}
}
CommunicationValue::new(CommunicationType::add_conversation)
}
pub async fn save(&self) {
let mut json = Object::new();
json.insert("name", JsonValue::String(self.name.clone()));
json.insert(
"owner_id",
JsonValue::Number(Number::from(*self.owner_id.read().await as i64)),
);
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() {
if let Ok(_) = permissions.push(perm.to_string()) {}
}
if let Ok(_) = 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<Arc<Community>> {
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<i64, Vec<Permission>> = HashMap::new();
for user in user_json.entries() {
let (id, json): (&str, &JsonValue) = user;
let perms_j = &json["permissions"];
let perms = Vec::new();
for _ in perms_j.entries() {
// let perm_j = i.as_str().unwrap();
// perms.push(perm_j.to_string());
}
let user_id: i64 = id.parse().unwrap_or(0);
users.push(user_id);
permissions.insert(user_id, perms);
}
let role_data = file_util::load_file(&format!("communities/{}/", name), "roles.json");
let roles: HashMap<String, Vec<Permission>> = HashMap::new();
if let Ok(_) = json::parse(&role_data) {
// Fill roles
} else {
return None;
};
let community = Community {
name: json_content["name"].as_str().unwrap().to_string(),
owner_id: Arc::new(RwLock::new(json_content["owner_id"].as_i64().unwrap_or(0))),
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<String> =
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<dyn Interactable> =
registry::load(comarc.clone(), String::new(), name).await;
comarc.add_interactable(Arc::new(interactable)).await;
}
}
Some(comarc)
}

View file

@ -1,396 +0,0 @@
use crate::auth::auth_user::AuthUser;
use crate::communities::community::Community;
use crate::communities::interactables::interactable::Interactable;
use crate::users::user_manager::get_user;
use aes_gcm::{Aes256Gcm, KeyInit, Nonce, aead::Aead};
use base64::{Engine as _, engine::general_purpose::STANDARD};
use futures::SinkExt;
use futures::stream::SplitSink;
use futures::stream::SplitStream;
use hkdf::Hkdf;
use hyper::upgrade::Upgraded;
use hyper_util::rt::TokioIo;
use json::JsonValue;
use json::number::Number;
use rand::{Rng, distributions::Alphanumeric};
use sha2::Sha256;
use std::sync::Arc;
use tokio::sync::RwLock;
use tokio_tungstenite::WebSocketStream;
use tungstenite::Message;
use tungstenite::Utf8Bytes;
use uuid::Uuid;
use x448::PublicKey;
pub struct CommunityConnection {
pub sender: Arc<RwLock<SplitSink<WebSocketStream<TokioIo<Upgraded>>, Message>>>,
pub receiver: Arc<RwLock<SplitStream<WebSocketStream<TokioIo<Upgraded>>>>>,
pub user_id: Arc<RwLock<i64>>,
pub community: Arc<RwLock<Option<Arc<Community>>>>,
identified: Arc<RwLock<bool>>,
challenged: Arc<RwLock<bool>>,
challenge: Arc<RwLock<String>>,
auth: Arc<RwLock<Option<AuthUser>>>,
pub ping: Arc<RwLock<i64>>,
}
impl CommunityConnection {
pub fn new(
sender: SplitSink<WebSocketStream<TokioIo<Upgraded>>, Message>,
receiver: SplitStream<WebSocketStream<TokioIo<Upgraded>>>,
community: Arc<Community>,
) -> Arc<Self> {
Arc::new(Self {
sender: Arc::new(RwLock::new(sender)),
receiver: Arc::new(RwLock::new(receiver)),
user_id: Arc::new(RwLock::new(0)),
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 sender = self.sender.write().await; // Access the SplitSink
let message_text = Message::Text(Utf8Bytes::from(message.to_json().to_string()));
sender.send(message_text).await.unwrap(); // Send the message via the SplitSink
}
pub async fn get_community(&self) -> Option<Arc<Community>> {
self.community.read().await.clone()
}
pub async fn get_user_id(&self) -> i64 {
*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<Self>, message: String) {
let mut cv = CommunicationValue::from_json(&message);
let user_id = self.get_user_id().await;
cv = cv.with_sender(user_id);
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) {
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, name, path, function, &cv)
.await;
self.send_message(&result).await;
}
async fn handle_identification(&self, cv: CommunicationValue) {
let user_id = cv
.get_data(DataTypes::user_id)
.unwrap_or(&JsonValue::Number(Number::from(0)))
.as_i64()
.unwrap_or(0);
let Some(user) = get_user(user_id) 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 = 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,
__ => {
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,
_ => {
self.send_error_response(&cv.get_id(), CommunicationType::error)
.await;
return;
}
};
let aes_key = {
let hk = Hkdf::<Sha256>::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<Self>, cv: CommunicationValue) {
let client_challenge_response_b64 = match cv.get_data(DataTypes::challenge) {
Some(data) => data.to_string(),
_ => {
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,
_ => {
self.send_error_response(&cv.get_id(), CommunicationType::error)
.await;
return;
}
};
let aes_key = {
use hkdf::Hkdf;
use sha2::Sha256;
let hk = Hkdf::<Sha256>::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 user_id = self.get_user_id().await;
if user_id == 0 {
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<Box<dyn Interactable>>> = 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 sender = self.sender.write().await;
let _ = sender.close().await;
}
pub async fn handle_close(self: Arc<Self>) {
if self.is_identified().await {
if self.get_user_id().await != 0 {
self.community
.read()
.await
.as_ref()
.unwrap()
.remove_connection(self.clone())
.await;
}
}
}
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::<i64>() {
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;
}
}

View file

@ -1,59 +0,0 @@
use crate::communities::community::{self, Community};
use crate::log;
use crate::util::file_util;
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;
pub static COMMUNITY_REGISTRY: Lazy<Arc<Mutex<HashMap<String, Arc<Community>>>>> =
Lazy::new(|| Arc::new(Mutex::new(HashMap::new())));
pub async fn add_community(community: Arc<Community>) {
COMMUNITY_REGISTRY
.lock()
.await
.insert(community.get_name().to_string(), community);
}
pub async fn remove_community(name: &str) {
COMMUNITY_REGISTRY.lock().await.remove(name);
}
pub async fn clear() {
COMMUNITY_REGISTRY.lock().await.clear();
}
pub async fn get_community(name: &str) -> Option<Arc<Community>> {
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!("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<Arc<Community>> {
COMMUNITY_REGISTRY.lock().await.values().cloned().collect()
}
pub async fn rename_community(old_name: &str, new_name: &str) {
if let Some(community) = COMMUNITY_REGISTRY.lock().await.remove(old_name) {
COMMUNITY_REGISTRY
.lock()
.await
.insert(new_name.to_string(), community);
}
}

View file

@ -1,121 +0,0 @@
use crate::communities::{community::Community, interactables::interactable::Interactable};
use async_trait::async_trait;
use json::JsonValue;
use std::any::Any;
use std::sync::Arc;
use ttp_core::CommunicationValue;
use uuid::Uuid;
pub struct Category {
id: Uuid,
name: String,
path: String,
community: Arc<Community>,
children: Vec<Arc<Box<dyn Interactable>>>,
}
impl Category {
pub fn new() -> Category {
Category {
id: Uuid::new_v4(),
name: String::new(),
path: String::new(),
community: Arc::new(Community::new()),
children: Vec::new(),
}
}
pub fn get_child(&self, path: String, name: String) -> Option<Arc<Box<dyn Interactable>>> {
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::<Category>().unwrap();
next_cat.get_child(path, name)
} else {
Some(next.clone())
}
}
}
pub fn get_children(&self) -> Vec<Arc<Box<dyn Interactable>>> {
self.children.iter().map(|child| child.clone()).collect()
}
}
#[async_trait]
impl Interactable for Category {
fn get_id(&self) -> &Uuid {
&self.id
}
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<Community> {
&self.community
}
fn set_community(&mut self, community: Arc<Community>) {
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
}
async 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 {
let _ = v["children"].push(child.to_json());
}
v
}
fn load(
&mut self,
community: Arc<Community>,
id: Uuid,
path: String,
name: String,
_json: &JsonValue,
) {
self.community = community;
self.id = id;
self.name = name;
self.path = path;
}
}

View file

@ -1,35 +0,0 @@
use crate::communities::community::Community;
use async_trait::async_trait;
use json::JsonValue;
use std::any::Any;
use std::sync::Arc;
use ttp_core::CommunicationValue;
use uuid::Uuid;
pub type InteractableFactory = fn() -> Box<dyn Interactable>;
#[async_trait]
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<Community>;
fn set_community(&mut self, community: Arc<Community>);
async fn run_function(&self, cv: CommunicationValue) -> CommunicationValue;
fn get_data(&self) -> JsonValue;
fn get_id(&self) -> &Uuid;
fn to_json(&self) -> JsonValue;
fn load(
&mut self,
community: Arc<Community>,
id: Uuid,
path: String,
name: String,
json: &JsonValue,
);
}

View file

@ -1,75 +0,0 @@
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::util::file_util;
use json::JsonValue;
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;
use uuid::Uuid;
pub static INTERACTABLE_REGISTRY: Lazy<Arc<Mutex<HashMap<String, InteractableFactory>>>> =
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<dyn Interactable>
});
INTERACTABLE_REGISTRY
.lock()
.await
.insert(VoiceChat::new().get_codec(), || {
Box::new(VoiceChat::new()) as Box<dyn Interactable>
});
INTERACTABLE_REGISTRY
.lock()
.await
.insert(Category::new().get_codec(), || {
Box::new(Category::new()) as Box<dyn Interactable>
});
}
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<dyn Interactable> {
INTERACTABLE_REGISTRY.lock().await.get(name).unwrap()()
}
pub async fn save(interactable: &Arc<Box<dyn Interactable>>) {
let mut json_object: JsonValue = interactable.to_json().clone();
json_object["codec"] = JsonValue::String(interactable.get_codec());
json_object["id"] = JsonValue::String(interactable.get_id().to_string());
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<Community>,
path: String,
name: String,
) -> Box<dyn Interactable + 'static> {
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 id: String = json_object["id"].as_str().unwrap().to_string();
let mut interactable = get_interactable(&codec).await;
interactable.load(c, Uuid::parse_str(&id).unwrap(), path, name, &json_object);
interactable
}

View file

@ -1,261 +0,0 @@
use crate::{
communities::{
community::Community, community_connection::CommunityConnection,
interactables::interactable::Interactable,
},
log,
util::file_util::{get_children, load_file, save_file},
};
use async_trait::async_trait;
use json::{JsonValue, array, object};
use std::fs;
use std::sync::Arc;
use std::{any::Any, collections::HashMap};
use ttp_core::{CommunicationType, CommunicationValue, DataTypes};
use uuid::Uuid;
pub struct TextChat {
id: Uuid,
name: String,
path: String,
community: Arc<Community>,
}
impl TextChat {
pub fn new() -> TextChat {
TextChat {
id: Uuid::new_v4(),
name: String::new(),
path: String::new(),
community: Arc::new(Community::new()),
}
}
pub fn add_message(&self, send_time: u128, sender: i64, 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!("Failed to create chat directory: {}", e);
return;
}
let mut chunk_index = 0;
let mut message_chunk = array![];
// find latest chunk not full (max 800 msgs)
loop {
let file_name = format!("msgs_{}.json", chunk_index);
let file_content = load_file(&user_dir, &file_name);
if !file_content.is_empty() {
if let Ok(current_chunk) = json::parse(&file_content) {
if current_chunk.is_array() && current_chunk.len() < 800 {
message_chunk = current_chunk;
break;
}
} else {
log!("Failed to parse existing JSON file: {}", file_name);
}
} else {
break;
}
chunk_index += 1;
if chunk_index > 1000 {
log!("Too many message chunks. Aborting add.");
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!("Failed to push new message into JSON array: {}", e);
return;
}
let file_name = format!("msgs_{}.json", chunk_index);
log!("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::<i32>() {
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
}
}
#[async_trait]
impl Interactable for TextChat {
fn get_id(&self) -> &Uuid {
&self.id
}
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<Community> {
&self.community
}
fn set_community(&mut self, community: Arc<Community>) {
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()
}
async fn run_function(&self, cv: CommunicationValue) -> CommunicationValue {
let payload = cv.get_data(DataTypes::payload).as_container().unwrap();
if cv.get_data(DataTypes::function).as_str().unwrap() == "get_messages" {
let amount = payload.get(DataTypes::amount).as_i64().unwrap();
let loaded_messages = payload["loaded_messages"].as_i64().unwrap();
let messages = self.get_messages(loaded_messages, amount).clone();
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(), message);
let mut distribution_payload = JsonValue::new_object();
distribution_payload["message"] = JsonValue::String(message.to_string());
distribution_payload["sender_id"] = JsonValue::String(cv.get_sender().to_string());
distribution_payload["send_time"] =
JsonValue::String(milliseconds_timestamp.to_string());
let distribution = CommunicationValue::new(CommunicationType::update)
.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_live".to_string())
.add_data(DataTypes::payload, distribution_payload);
let connections: HashMap<i64, Vec<Arc<CommunityConnection>>> =
self.get_community().get_connections().await.clone();
for con in connections.values() {
for c in con {
let cd: &Arc<CommunityConnection> = c;
cd.send_message(&distribution).await;
}
}
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 {
JsonValue::new_object()
}
fn load(
&mut self,
community: Arc<Community>,
id: Uuid,
path: String,
name: String,
_json: &JsonValue,
) {
self.community = community;
self.id = id;
self.name = name;
self.path = path;
}
}

View file

@ -1,187 +0,0 @@
use crate::communities::{community::Community, interactables::interactable::Interactable};
use async_trait::async_trait;
use json::JsonValue;
use std::sync::Arc;
use std::{any::Any, sync::RwLock};
use uuid::Uuid;
pub enum CallUserState {
Active,
Muted,
Deafed,
}
impl CallUserState {
pub fn parse(state: &str) -> CallUserState {
match state {
"active" => CallUserState::Active,
"muted" => CallUserState::Muted,
"deafed" => CallUserState::Deafed,
_ => CallUserState::Active,
}
}
pub fn to_string(&self) -> String {
match self {
CallUserState::Active => "active".to_string(),
CallUserState::Muted => "muted".to_string(),
CallUserState::Deafed => "deafed".to_string(),
}
}
}
pub struct CallUser {
pub user_id: Uuid,
pub user_state: CallUserState,
pub streaming: bool,
}
pub struct VoiceChat {
id: Uuid,
name: String,
path: String,
community: Arc<Community>,
users: RwLock<Vec<CallUser>>,
}
impl VoiceChat {
pub fn new() -> VoiceChat {
VoiceChat {
id: Uuid::new_v4(),
name: String::new(),
path: String::new(),
community: Arc::new(Community::new()),
users: RwLock::new(Vec::new()),
}
}
pub fn update_user_state(
self: Arc<Self>,
user_id: Uuid,
state: CallUserState,
streaming: bool,
) {
if let Some(user) = self
.users
.write()
.unwrap()
.iter_mut()
.find(|u| u.user_id == user_id)
{
user.user_state = state;
user.streaming = streaming;
}
}
}
#[async_trait]
impl Interactable for VoiceChat {
fn get_id(&self) -> &Uuid {
&self.id
}
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<Community> {
&self.community
}
fn set_community(&mut self, community: Arc<Community>) {
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 data = JsonValue::new_object();
let mut active_users = JsonValue::new_object();
for user in self.users.read().unwrap().iter() {
let mut user_data = JsonValue::new_object();
let _ = user_data.insert("state", JsonValue::String(user.user_state.to_string()));
let _ = user_data.insert("streaming", JsonValue::Boolean(user.streaming));
let _ = active_users.insert(&user.user_id.to_string(), user_data);
}
let _ = data.insert("active_users", active_users);
data
}
async fn run_function(&self, cv: CommunicationValue) -> CommunicationValue {
let payload = cv.get_data(DataTypes::payload).unwrap();
let function = cv.get_data(DataTypes::function).unwrap().as_str().unwrap();
if function == "get_call" {
let sender_id = payload["sender_id"].as_str().unwrap();
let message_id = payload["message"].as_str().unwrap();
let send_time = payload["send_time"].as_str().unwrap();
let mut response_payload = JsonValue::new_object();
response_payload["sender_id"] = JsonValue::String(sender_id.to_string());
response_payload["message"] = JsonValue::String(message_id.to_string());
response_payload["send_time"] = JsonValue::String(send_time.to_string());
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, "getting_call".to_string())
.add_data(DataTypes::payload, response_payload);
}
if function == "update_user_state" {
let user_id = payload["user_id"].as_str().unwrap();
let state = payload["state"].as_str().unwrap();
let streaming = payload["streaming"].as_bool().unwrap();
if let Some(user) = self
.users
.write()
.unwrap()
.iter_mut()
.find(|u| u.user_id == Uuid::parse_str(user_id).unwrap())
{
user.user_state = CallUserState::parse(state);
user.streaming = streaming;
}
let mut response_payload = JsonValue::new_object();
response_payload["user_id"] = JsonValue::String(user_id.to_string());
response_payload["state"] = JsonValue::String(state.to_string());
response_payload["streaming"] = JsonValue::Boolean(streaming);
return CommunicationValue::new(CommunicationType::update)
.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, "user_changed".to_string())
.add_data(DataTypes::payload, response_payload);
}
CommunicationValue::new(CommunicationType::error).with_id(cv.get_id())
}
fn to_json(&self) -> JsonValue {
let v = JsonValue::new_object();
v
}
fn load(
&mut self,
community: Arc<Community>,
id: Uuid,
path: String,
name: String,
_json: &JsonValue,
) {
self.community = community;
self.id = id;
self.name = name;
self.path = path;
}
}

View file

@ -1,13 +0,0 @@
pub mod community_manager;
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;
pub mod perms {
pub mod permission;
}

View file

@ -1,27 +0,0 @@
use json::JsonValue;
use uuid::Uuid;
pub struct Permission {
pub id: Uuid,
pub name: String,
}
impl Permission {
pub fn new(id: Uuid, name: String) -> Self {
Permission { id, name }
}
pub fn to_json(&self) -> JsonValue {
json::object! {
"id" => self.id.to_string(),
"name" => self.name.clone()
}
}
pub fn from_json(json: JsonValue) -> Self {
Permission {
id: Uuid::parse_str(json["id"].as_str().unwrap()).unwrap(),
name: json["name"].as_str().unwrap().to_string(),
}
}
pub fn to_string(&self) -> String {
self.to_json().as_str().unwrap().to_string()
}
}

1
src/data/mod.rs Normal file
View file

@ -0,0 +1 @@
pub mod user;

30
src/data/user.rs Normal file
View file

@ -0,0 +1,30 @@
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
#[derive(Debug, Clone, PartialEq, EnumIter, Eq)]
#[allow(unused, non_camel_case_types)]
pub enum UserStatus {
user_offline,
user_online,
user_dnd,
user_idle,
user_wc,
user_borked,
iota_offline,
iota_online,
iota_borked,
}
#[allow(unused)]
impl UserStatus {
pub fn to_string(&self) -> String {
format!("{:?}", self)
}
pub fn from_str(s: &str) -> Option<UserStatus> {
for sel in UserStatus::iter() {
if &sel.to_string() == s {
return Some(sel);
}
}
None
}
}

View file

@ -1,201 +0,0 @@
use crate::{ACTIVE_TASKS, APP_STATE, SHUTDOWN, gui::elements::log_card::UiLogEntry};
use json::{JsonValue, object};
use std::{collections::VecDeque, thread, time::Duration};
use sysinfo::{RefreshKind, System};
#[derive(Clone)]
pub struct AppState {
pub logs: VecDeque<UiLogEntry>,
pub cpu: Vec<(f64, f64)>,
pub ram: Vec<(f64, f64)>,
pub ping: Vec<(f64, f64)>,
pub net_up: Vec<(f64, f64)>,
pub net_down: Vec<(f64, f64)>,
pub sys_info: String,
}
const MAX_POINTS: usize = 1000;
const MAX_LOGS: usize = 100;
impl AppState {
pub fn new() -> Self {
Self {
logs: VecDeque::new(),
cpu: Vec::new(),
ram: Vec::new(),
ping: Vec::new(),
net_up: Vec::new(),
net_down: Vec::new(),
sys_info: String::from("Loading..."),
}
}
pub fn push_log(&mut self, msg: UiLogEntry) {
if self.logs.len() >= MAX_LOGS {
self.logs.pop_front();
}
self.logs.push_back(msg);
}
pub fn get_logs(&self) -> &VecDeque<UiLogEntry> {
&self.logs
}
pub fn push_cpu(&mut self, pt: (f64, f64)) {
self.cpu.push(pt);
if self.cpu.len() > MAX_POINTS {
self.cpu.remove(0);
}
}
pub fn push_ram(&mut self, pt: (f64, f64)) {
self.ram.push(pt);
if self.ram.len() > MAX_POINTS {
self.ram.remove(0);
}
}
pub fn push_ping_val(&mut self, pt: f64) {
self.ping.push((self.ping.len() as f64, pt));
if self.ping.len() > MAX_POINTS {
self.ping.remove(0);
}
}
pub fn push_net_up(&mut self, pt: (f64, f64)) {
self.net_up.push(pt);
if self.net_up.len() > MAX_POINTS {
self.net_up.remove(0);
}
}
pub fn push_net_down(&mut self, pt: (f64, f64)) {
self.net_down.push(pt);
if self.net_down.len() > MAX_POINTS {
self.net_down.remove(0);
}
}
pub fn to_json(&self) -> JsonValue {
let json = object! {
"cpu" => self.cpu
.iter()
.map(|(_, y)| *y)
.collect::<Vec<f64>>(),
"ram" => self.ram
.iter()
.map(|(_, y)| *y)
.collect::<Vec<f64>>(),
"ping" => self
.ping
.iter()
.map(|(_, y)| *y)
.collect::<Vec<f64>>(),
"net_up" => self
.net_up
.iter()
.map(|(_, y)| *y)
.collect::<Vec<f64>>(),
"net_down" => self
.net_down
.iter()
.map(|(_, y)| *y)
.collect::<Vec<f64>>(),
};
json
}
pub fn with_width(&self, width: u16) -> Self {
let mut new = self.clone();
new.cpu = Self::downsample_to_fit_width(&new.cpu, width);
new.ram = Self::downsample_to_fit_width(&new.ram, width);
new.ping = Self::downsample_to_fit_width(&new.ping, width);
new.net_up = Self::downsample_to_fit_width(&new.net_up, width);
new.net_down = Self::downsample_to_fit_width(&new.net_down, width);
new
}
fn downsample_to_fit_width(data: &[(f64, f64)], width: u16) -> Vec<(f64, f64)> {
let width_usize = (width as usize) * 2;
let len = data.len();
if len >= width_usize {
data[len - width_usize..].to_vec()
} else {
let mut result = Vec::with_capacity(width_usize);
let dx = 1.0;
let pad_len = width_usize - len;
let start_x = data
.first()
.map(|(x, _)| x - (dx * pad_len as f64))
.unwrap_or(0.0);
let _ = data.first().map(|(_, y)| *y).unwrap_or(0.0);
for i in 0..pad_len {
result.push((start_x + i as f64 * dx, -1 as f64));
}
result.extend_from_slice(data);
result
}
}
}
pub fn setup() {
ACTIVE_TASKS.insert("System info loader".to_string());
tokio::spawn(async move {
let mut sys = System::new_with_specifics(RefreshKind::everything());
let mut last_total_received = 0u64;
let mut last_total_transmitted = 0u64;
let mut counter = 0.0;
loop {
if *SHUTDOWN.read().await {
break;
}
sys.refresh_all();
let mut tcpu = 0;
for cpu in sys.cpus() {
tcpu += cpu.cpu_usage() as i64;
tcpu /= 2;
}
let ram = (sys.used_memory() as f64 / sys.total_memory() as f64) * 100.0;
let total_received = 0u64;
let total_transmitted = 0u64;
let delta_received = if last_total_received == 0 {
0
} else {
total_received.saturating_sub(last_total_received)
};
let delta_transmitted = if last_total_transmitted == 0 {
0
} else {
total_transmitted.saturating_sub(last_total_transmitted)
};
last_total_received = total_received;
last_total_transmitted = total_transmitted;
let net_down = delta_received as f64;
let net_up = delta_transmitted as f64;
{
let mut st = APP_STATE.lock().unwrap();
st.push_cpu((counter, tcpu as f64));
st.push_ram((counter, ram));
st.push_net_down((counter, net_down));
st.push_net_up((counter, net_up));
st.sys_info = format!("NetDown: {} NetUp: {}", delta_received, delta_transmitted);
}
counter += 1.0;
if counter > 30.0 {
thread::sleep(Duration::from_millis(500));
} else {
thread::sleep(Duration::from_millis(5));
}
}
ACTIVE_TASKS.remove("System info loader");
});
}

View file

@ -1,491 +0,0 @@
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{
Frame,
layout::Rect,
style::{Color, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
};
use ttp_core::{CommunicationType, CommunicationValue};
use uuid::Uuid;
use crate::{
ACTIVE_TASKS, RELOAD, SHUTDOWN,
gui::{
elements::elements::{Element, InteractableElement, JoinableElement},
interaction_result::InteractionResult,
ui::FPS,
util::borders::draw_block_joins,
},
log, log_command, log_cv,
omikron::omikron_connection::OMIKRON_CONNECTION,
users::{user_manager, user_profile::UserProfile},
util::file_util,
};
use std::{
any::Any,
sync::{Arc, Mutex},
time::Duration,
};
use tokio::time::Instant;
pub struct ConsoleCard {
focused: bool,
pub title: String,
pub content: String,
pub cursor_position: usize,
borders: Borders,
joins: Borders,
cursor: Arc<Mutex<bool>>,
last_swap: Arc<Mutex<Instant>>,
tab_index: usize,
}
impl ConsoleCard {
pub fn new(title: &str, content: &str) -> Self {
ConsoleCard {
focused: false,
title: title.to_string(),
content: content.to_string(),
cursor_position: content.chars().count(),
borders: Borders::ALL,
joins: Borders::NONE,
cursor: Arc::new(Mutex::new(true)),
last_swap: Arc::new(Mutex::new(Instant::now())),
tab_index: 0,
}
}
fn byte_index(&self) -> usize {
self.content
.char_indices()
.nth(self.cursor_position)
.map(|(i, _)| i)
.unwrap_or(self.content.len())
}
fn cursor_visible(&self) -> bool {
if !self.focused {
return false;
}
let mut visible = self.cursor.lock().unwrap();
let mut last = self.last_swap.lock().unwrap();
let now = Instant::now();
if now.duration_since(*last) >= Duration::from_millis(500) {
*visible = !*visible;
*last = now;
}
*visible
}
fn current_prefix(&self) -> Option<&str> {
if self.content.starts_with('/') {
Some("/")
} else {
None
}
}
fn cursor_spans(&self) -> Vec<Span<'static>> {
let cursor_visible = self.cursor_visible();
let cursor_style = Style::default().fg(Color::White).bg(Color::DarkGray);
let mut spans = Vec::new();
if self.content.is_empty() {
if self.focused {
if cursor_visible {
spans.push(Span::styled(" ", cursor_style));
} else {
spans.push(Span::styled(" ", Style::default().fg(Color::White)));
}
spans.push(Span::styled(
"send command (<help> for info)",
Style::default().fg(Color::DarkGray),
));
} else {
spans.push(Span::styled(
" send command (<help> for info)",
Style::default().fg(Color::DarkGray),
));
}
return spans;
}
let byte_index = self.byte_index();
let before = self.content[..byte_index].to_string();
let after = self.content[byte_index..].to_string();
let prefix_len = self.current_prefix().map(|s| s.len()).unwrap_or(0);
if prefix_len > 0 && before.len() >= prefix_len {
let prefix = &before[..prefix_len];
let rest = &before[prefix_len..];
spans.push(Span::styled(
prefix.to_string(),
Self::style_for_part(true, false, false),
));
if !rest.is_empty() {
spans.push(Span::styled(
rest.to_string(),
Style::default().fg(Color::White),
));
}
} else if !before.is_empty() {
spans.push(Span::styled(
before.clone(),
Style::default().fg(Color::White),
));
}
if cursor_visible {
spans.push(Span::styled(" ", cursor_style));
}
if !after.is_empty() {
spans.push(Span::styled(after, Style::default().fg(Color::White)));
}
spans
}
fn style_for_part(is_prefix: bool, is_hint: bool, is_error: bool) -> Style {
if is_error {
return Style::default().fg(Color::Red);
}
if is_hint {
return Style::default().fg(Color::DarkGray);
}
if is_prefix {
return Style::default().fg(Color::DarkGray);
}
Style::default().fg(Color::White)
}
fn render_cursor_spans(&self) -> Vec<Span<'static>> {
self.cursor_spans()
}
fn move_cursor_left(&mut self) {
if self.cursor_position > 0 {
self.cursor_position -= 1;
}
}
fn move_cursor_right(&mut self) {
let len = self.content.chars().count();
if self.cursor_position < len {
self.cursor_position += 1;
}
}
fn delete_at_cursor(&mut self) {
if self.content.is_empty() || self.cursor_position == 0 {
return;
}
let start = self
.content
.char_indices()
.nth(self.cursor_position.saturating_sub(1))
.map(|(i, _)| i)
.unwrap_or(0);
let end = self.byte_index();
self.content.replace_range(start..end, "");
self.cursor_position -= 1;
}
fn insert_at_cursor(&mut self, c: char) {
let idx = self.byte_index();
self.content.insert(idx, c);
self.cursor_position += 1;
}
}
impl Element for ConsoleCard {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn render(&self, f: &mut Frame, r: Rect) {
let block = Block::default()
.borders(self.borders)
.title(self.title.clone())
.title_style(Style::default().fg(Color::White))
.border_style(if self.focused {
Style::default().fg(Color::Yellow)
} else {
Style::default()
})
.style(if self.focused {
Style::default().fg(Color::White)
} else {
Style::default()
});
let spans = self.render_cursor_spans();
let par = Paragraph::new(Line::from(spans))
.block(block)
.scroll((0, 0));
f.render_widget(par, r);
draw_block_joins(f, r, self.borders, self.joins);
}
}
impl JoinableElement for ConsoleCard {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn as_element(&self) -> &(dyn Element + 'static) {
self
}
fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) {
self
}
fn set_borders(&mut self, borders: Borders) {
self.borders = borders;
}
fn set_joins(&mut self, joins: Borders) {
self.joins = joins;
}
}
impl InteractableElement for ConsoleCard {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn as_element(&self) -> &(dyn Element + 'static) {
self
}
fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) {
self
}
fn interact(&mut self, key: KeyEvent) -> InteractionResult {
match key.code {
KeyCode::Enter => {
if self.content.is_empty() {
log!("");
return InteractionResult::Handled;
}
let command = self.content.clone();
let id = Uuid::new_v4();
let id = id.to_string();
let id = id.split_at(8).0;
let task_id = format!("command_{}_{}", command, id);
ACTIVE_TASKS.insert(task_id.clone());
log_command!("{}", command);
tokio::spawn(async move {
run_command(&command).await;
ACTIVE_TASKS.remove(&task_id);
});
self.content.clear();
self.cursor_position = 0;
self.tab_index = 0;
InteractionResult::Handled
}
KeyCode::Backspace => {
self.delete_at_cursor();
InteractionResult::Handled
}
KeyCode::Delete => {
let len = self.content.chars().count();
if self.cursor_position < len {
let start = self.byte_index();
let end = self
.content
.char_indices()
.nth(self.cursor_position + 1)
.map(|(i, _)| i)
.unwrap_or(self.content.len());
self.content.replace_range(start..end, "");
}
InteractionResult::Handled
}
KeyCode::Left => {
self.move_cursor_left();
InteractionResult::Handled
}
KeyCode::Right => {
self.move_cursor_right();
InteractionResult::Handled
}
KeyCode::Home => {
self.cursor_position = 0;
InteractionResult::Handled
}
KeyCode::End => {
self.cursor_position = self.content.chars().count();
InteractionResult::Handled
}
KeyCode::Tab => {
if let Some(prefix) = self.current_prefix() {
if prefix == "/" {
self.tab_index = self.tab_index.saturating_add(1);
}
}
InteractionResult::Handled
}
_ => {
if let Some(c) = key.code.as_char() {
self.insert_at_cursor(c);
InteractionResult::Handled
} else {
InteractionResult::Unhandled
}
}
}
}
fn can_focus(&self) -> bool {
true
}
fn is_focused(&self) -> bool {
self.focused
}
fn focus(&mut self, f: bool) {
self.focused = f;
}
}
pub async fn run_command(command: &str) {
let parts = command.split(" ").collect::<Vec<&str>>();
match parts.as_slice() {
["tasks"] => {
let active_tasks: Vec<String> =
ACTIVE_TASKS.clone().iter().map(|v| v.to_string()).collect();
let info = if *SHUTDOWN.read().await && *RELOAD.read().await {
"Rebooting, "
} else if *SHUTDOWN.read().await {
"Shutting , "
} else {
""
};
log!("{}Active tasks: {:?}", info, active_tasks);
}
["fps"] => {
let (fps, skips) = *FPS.read().await;
log!("{:.1} FPS with {:.1}% of attempts skipped", fps, skips);
}
["help"] => {
log!("Available commands: tasks, fps, ping, user");
}
["help", "tasks"] => {
log!("Tasks command usage: tasks");
}
["help", "fps"] => {
log!("FPS command usage: fps");
}
["help", "ping"] => {
log!("Ping command usage: ping [time]");
}
["help", "user"] => {
log!("User command usage: user add <username> | user remove <username> | user list");
}
["ping"] => {
ping(20).await;
}
["ping", time] => {
let time = time.parse::<u64>().unwrap_or(20);
ping(time).await;
}
["user", "add", username] => {
if let (Some(user), Some(_)) = user_manager::create_user(username).await {
log!("Created user {}", user.user_id);
} else {
log!("Failed to create user");
}
}
["user", "remove", username] => {
if let Some(user) = user_manager::get_user_by_username(username) {
user_manager::remove_user(user.user_id);
log!("Removed user {}", user.user_id);
} else {
log!("Failed to find user");
}
}
["user", "list"] => {
let users: Vec<UserProfile> = user_manager::get_users();
for user in users {
let storage = file_util::get_designed_storage(user.user_id);
log!(
"> Username: {}, ID: {}, created at: {}, storage: {}",
user.username,
user.user_id,
user.created_at,
storage
);
}
}
["user", "info", username] => {
if let Some(user) = user_manager::get_user_by_username(username) {
user_manager::remove_user(user.user_id);
log!("Removed user {}", user.user_id);
} else {
log!("Failed to find user");
}
}
["reload"] | ["restart"] => {
log!("Restarting");
*RELOAD.write().await = true;
*SHUTDOWN.write().await = true;
}
["shutdown"] | ["stop"] => {
log!("Shutting down");
*SHUTDOWN.write().await = true;
}
_ => {
log!("Unknown command");
}
}
}
pub async fn ping(time: u64) {
let conn = OMIKRON_CONNECTION.clone();
let response_cv = conn
.await_response(
&CommunicationValue::new(CommunicationType::ping),
Some(Duration::from_secs(time)),
)
.await;
match response_cv {
Ok(response) => log_cv!(response),
Err(err) => log!("Ping error: {:?}", err),
}
}

View file

@ -1,49 +0,0 @@
use std::any::Any;
use crossterm::event::KeyEvent;
use ratatui::{Frame, layout::Rect, widgets::Borders};
use crate::gui::{interaction_result::InteractionResult, screens::screens::Screen};
#[allow(unused)]
pub trait Element: Send + Sync + Any {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn render(&self, f: &mut Frame, r: Rect);
}
#[allow(unused)]
pub trait JoinableElement: Send + Sync + Any {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn as_element(&self) -> &dyn Element;
fn as_element_mut(&mut self) -> &mut dyn Element;
fn set_borders(&mut self, borders: Borders);
fn set_joins(&mut self, joins: Borders);
}
#[allow(unused)]
pub trait InfoElement: Send + Sync + Any {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn as_element(&self) -> &dyn Element;
fn as_element_mut(&mut self) -> &mut dyn Element;
fn get_info_screen(&self) -> Box<dyn Screen>;
}
#[allow(unused)]
pub trait InteractableElement: Send + Sync + Any {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn as_element(&self) -> &dyn Element;
fn as_element_mut(&mut self) -> &mut dyn Element;
fn interact(&mut self, key: KeyEvent) -> InteractionResult;
fn can_focus(&self) -> bool;
fn is_focused(&self) -> bool;
fn focus(&mut self, f: bool);
}

View file

@ -1,215 +0,0 @@
use std::{any::Any, sync::Arc};
use crossterm::event::KeyEvent;
use ratatui::{
Frame,
layout::Rect,
style::{Color, Style},
widgets::{
Block, Borders,
canvas::{Canvas, Line},
},
};
use crate::{
APP_STATE,
gui::{
elements::elements::{Element, InteractableElement, JoinableElement},
interaction_result::InteractionResult,
ui::UI,
util::borders::draw_block_joins,
},
};
pub enum GRAPHS {
Ram,
Cpu,
Ping,
}
impl GRAPHS {
pub fn get_color(&self) -> Color {
match self {
GRAPHS::Ram => Color::Blue,
GRAPHS::Cpu => Color::Red,
GRAPHS::Ping => Color::Green,
}
}
pub fn get_graph(&self) -> Vec<(f64, f64)> {
match self {
GRAPHS::Ram => APP_STATE.lock().unwrap().with_width(28).ram.clone(),
GRAPHS::Cpu => APP_STATE.lock().unwrap().with_width(28).cpu.clone(),
GRAPHS::Ping => APP_STATE.lock().unwrap().with_width(28).ping.clone(),
}
}
pub fn get_unit(&self) -> String {
match self {
GRAPHS::Ram => "MB".to_string(),
GRAPHS::Cpu => "%".to_string(),
GRAPHS::Ping => "ms".to_string(),
}
}
}
#[allow(unused)]
pub struct GraphCard {
ui: Arc<UI>,
graph_type: GRAPHS,
focused: bool,
pub title: String,
borders: Borders,
joins: Borders,
open: bool,
}
impl GraphCard {
pub fn new(ui: Arc<UI>, graph_type: GRAPHS, title: String) -> Self {
Self {
ui,
graph_type,
focused: false,
title,
borders: Borders::ALL,
joins: Borders::NONE,
open: true,
}
}
pub fn set_open(&mut self, open: bool) {
self.open = open;
}
}
impl Element for GraphCard {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn render(&self, f: &mut Frame, r: Rect) {
if self.open {
let graph = self.graph_type.get_graph();
let unit = self.graph_type.get_unit();
let min_x = graph.first().map(|(x, _)| *x).unwrap_or(0.0);
let max_x = graph.last().map(|(x, _)| *x).unwrap_or(100.0);
let min_y = graph
.iter()
.map(|(_, y)| *y)
.filter(|y| *y > 0.0)
.min_by(|a, b| a.total_cmp(b))
.unwrap_or(0.0);
let max_y = graph.iter().map(|(_, y)| *y).fold(-1.0, f64::max);
let block = Block::default()
.title(format!(
"{}:─{}{}─{}min/{}max",
self.title,
graph.last().unwrap_or(&(0.0, 0.0)).1 as i64,
unit,
min_y as i64,
max_y as i64,
))
.borders(self.borders)
.border_style(if self.focused {
Style::default().fg(Color::Yellow)
} else {
Style::default()
});
let canvas = Canvas::default()
.block(block)
.x_bounds([min_x, max_x])
.y_bounds([0.0, 100.0])
.paint(|ctx| {
for (x, y) in &graph {
ctx.draw(&Line {
x1: *x,
y1: 0.0,
x2: *x,
y2: *y,
color: self.graph_type.get_color(),
});
}
});
f.render_widget(canvas, r);
} else {
let block = Block::default()
.title("")
.borders(self.borders)
.border_style(if self.focused {
Style::default().fg(Color::Yellow)
} else {
Style::default()
});
f.render_widget(block, r);
}
draw_block_joins(f, r, self.borders, self.joins);
}
}
impl JoinableElement for GraphCard {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn as_element(&self) -> &dyn Element {
self
}
fn as_element_mut(&mut self) -> &mut dyn Element {
self
}
fn set_borders(&mut self, borders: Borders) {
self.borders = borders;
}
fn set_joins(&mut self, joins: Borders) {
self.joins = joins;
}
}
impl InteractableElement for GraphCard {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn as_element(&self) -> &dyn Element {
self
}
fn as_element_mut(&mut self) -> &mut dyn Element {
self
}
fn interact(&mut self, _key: KeyEvent) -> InteractionResult {
InteractionResult::Handled
}
fn can_focus(&self) -> bool {
true
}
fn is_focused(&self) -> bool {
self.focused
}
fn focus(&mut self, f: bool) {
self.focused = f;
}
}

View file

@ -1,483 +0,0 @@
use crate::APP_STATE;
use crate::gui::elements::elements::{Element, InteractableElement, JoinableElement};
use crate::gui::interaction_result::InteractionResult;
use crate::gui::util::borders::draw_block_joins;
use crate::util::logger::PrintType;
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{
Frame,
layout::Rect,
style::{Color, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
};
use std::any::Any;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Clone, Debug)]
pub struct UiLogEntry {
pub timestamp_ms: u128,
pub sender: PrintType,
pub message: String,
pub is_error: bool,
}
impl UiLogEntry {
pub fn format_timestamp(&self) -> String {
let secs = (self.timestamp_ms / 1000) as i64;
let hours = (secs / 3600) % 24;
let minutes = (secs / 60) % 60;
let seconds = secs % 60;
format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
}
}
impl From<LogEntry> for UiLogEntry {
fn from(entry: LogEntry) -> Self {
Self {
timestamp_ms: entry.timestamp_ms,
sender: entry.sender,
message: entry.message,
is_error: entry.is_error,
}
}
}
#[derive(Clone, Debug)]
pub struct LogEntry {
pub timestamp_ms: u128,
pub sender: PrintType,
pub message: String,
pub is_error: bool,
}
impl LogEntry {
pub fn new(sender: PrintType, message: String, is_error: bool) -> Self {
Self {
timestamp_ms: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis(),
sender,
message,
is_error,
}
}
}
pub struct LogCard {
focused: bool,
selected: bool,
scroll_offset: usize,
last_total_lines: usize,
last_visible_height: usize,
pub borders: Borders,
pub joins: Borders,
}
impl LogCard {
pub fn new() -> Self {
Self {
focused: false,
selected: false,
scroll_offset: 0,
last_total_lines: 0,
last_visible_height: 10,
borders: Borders::ALL,
joins: Borders::NONE,
}
}
fn get_logs(&self) -> Vec<UiLogEntry> {
let state = APP_STATE.lock().unwrap();
state.get_logs().iter().cloned().collect()
}
fn find_split_point(s: &str, max_width: usize) -> usize {
if max_width == 0 {
return s.len();
}
let mut current_width = 0usize;
let mut last_boundary = 0usize;
for (idx, ch) in s.char_indices() {
let char_width = if ch.is_ascii() { 1 } else { 2 };
if current_width + char_width > max_width {
if last_boundary == 0 {
return idx + ch.len_utf8();
}
return last_boundary;
}
current_width += char_width;
last_boundary = idx + ch.len_utf8();
}
s.len()
}
fn wrap_entry(entry: &UiLogEntry, available_width: usize) -> Vec<(String, Color, bool)> {
let mut result = Vec::new();
let timestamp = entry.format_timestamp();
let first_prefix = "";
let default_prefix = "";
let last_prefix = "";
let prefix_width = 2;
let first_line_width = available_width.saturating_sub(prefix_width + 1);
let continuation_width = available_width.saturating_sub(prefix_width);
let segments: Vec<&str> = entry.message.split('\n').collect();
let mut raw_lines = Vec::new();
for segment in segments {
let mut remaining = segment;
if remaining.is_empty() {
raw_lines.push(String::new());
continue;
}
let mut is_first_part = true;
while !remaining.is_empty() {
let current_width = if is_first_part {
first_line_width
} else {
continuation_width
};
let split_point = Self::find_split_point(remaining, current_width);
let line_content = remaining[..split_point].to_string();
raw_lines.push(line_content);
remaining = &remaining[split_point..];
is_first_part = false;
}
}
if raw_lines.is_empty() {
raw_lines.push(String::new());
}
for (idx, content) in raw_lines.iter().enumerate() {
let is_last = idx + 1 == raw_lines.len();
let is_single = raw_lines.len() == 1;
let is_first = idx == 0;
let prefix = if is_first {
first_prefix
} else if is_last && !is_single {
last_prefix
} else {
default_prefix
};
let mut line = String::from(prefix);
line.push_str(content);
if is_last && !timestamp.is_empty() {
line.push(' ');
line.push_str(&timestamp);
}
result.push((line, entry.sender.prefix_color(), entry.is_error));
}
result
}
fn build_all_lines(
&self,
entries: Vec<UiLogEntry>,
width: usize,
) -> Vec<(String, Color, bool)> {
let mut lines = Vec::new();
for entry in entries {
let wrapped = Self::wrap_entry(&entry, width);
lines.extend(wrapped);
}
lines
}
fn calculate_view_window(&self, total_lines: usize, visible_height: usize) -> (usize, usize) {
if total_lines <= visible_height {
return (0, total_lines);
}
let max_offset = total_lines - visible_height;
let clamped_offset = self.scroll_offset.min(max_offset);
let end = total_lines - clamped_offset;
let start = end.saturating_sub(visible_height);
(start, end)
}
fn get_title_hints(&self) -> (bool, bool) {
if self.last_total_lines == 0 || self.last_total_lines <= self.last_visible_height {
return (false, false);
}
let max_offset = self.last_total_lines - self.last_visible_height;
let can_scroll_up = self.scroll_offset < max_offset;
let can_scroll_down = self.scroll_offset > 0;
(can_scroll_up, can_scroll_down)
}
fn build_title(&self) -> String {
if !self.focused {
return "Logs".to_string();
}
let (can_up, can_down) = self.get_title_hints();
if !can_up && !can_down {
return "Logs".to_string();
}
let nav_symbol = if self.selected { "" } else { "j" };
let down_symbol = if self.selected { "" } else { "k" };
match (can_up, can_down) {
(true, true) => format!("Logs ({} older {} newer)", nav_symbol, down_symbol),
(true, false) => format!("Logs ({} older)", nav_symbol),
(false, true) => format!("Logs ({} newer)", down_symbol),
(false, false) => "Logs".to_string(),
}
}
fn scroll_up(&mut self) {
let max_offset = self
.last_total_lines
.saturating_sub(self.last_visible_height);
self.scroll_offset = (self.scroll_offset + 1).min(max_offset);
}
fn scroll_down(&mut self) {
self.scroll_offset = self.scroll_offset.saturating_sub(1);
}
fn split_line_prefix(line: &str) -> (&str, &str) {
if let Some(rest) = line.strip_prefix("") {
("", rest)
} else if let Some(rest) = line.strip_prefix("") {
("", rest)
} else if let Some(rest) = line.strip_prefix("") {
("", rest)
} else {
("", line)
}
}
fn split_timestamp_suffix(line: &str) -> (&str, &str) {
if let Some(idx) = line.rfind(' ') {
let possible_timestamp = &line[idx + 1..];
if possible_timestamp.len() == 8
&& possible_timestamp.as_bytes()[2] == b':'
&& possible_timestamp.as_bytes()[5] == b':'
{
let (content, timestamp_with_space) = line.split_at(idx);
return (content.trim_end(), timestamp_with_space);
}
}
(line, "")
}
}
impl Element for LogCard {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn render(&self, f: &mut Frame, area: Rect) {
let entries = self.get_logs();
let block = Block::default()
.title(self.build_title())
.borders(self.borders)
.border_style(if self.focused {
Style::default().fg(Color::Yellow)
} else {
Style::default()
});
let inner_area = block.inner(area);
f.render_widget(block, area);
if inner_area.width == 0 || inner_area.height == 0 {
draw_block_joins(f, area, self.borders, self.joins);
return;
}
let all_lines = self.build_all_lines(entries, inner_area.width as usize);
let total_lines = all_lines.len();
let visible_height = inner_area.height as usize;
let (start, end) = self.calculate_view_window(total_lines, visible_height);
let visible_lines = &all_lines[start..end];
let rendered_lines: Vec<Line> = visible_lines
.iter()
.map(|(line, prefix_color, is_error)| {
let mut spans = Vec::new();
let (prefix, rest) = Self::split_line_prefix(line);
if !prefix.is_empty() {
spans.push(Span::styled(
prefix.to_string(),
Style::default().fg(*prefix_color),
));
}
let (content, timestamp) = Self::split_timestamp_suffix(rest);
let text_color = if *is_error { Color::Red } else { Color::White };
if !content.is_empty() {
spans.push(Span::styled(
content.to_string(),
Style::default().fg(text_color),
));
}
if !timestamp.is_empty() {
spans.push(Span::styled(
timestamp.to_string(),
Style::default().fg(Color::DarkGray),
));
}
Line::from(spans)
})
.collect();
for (idx, line) in rendered_lines.iter().enumerate() {
let line_area = Rect {
x: inner_area.x,
y: inner_area.y + idx as u16,
width: inner_area.width,
height: 1,
};
f.render_widget(Paragraph::new(line.clone()), line_area);
}
draw_block_joins(f, area, self.borders, self.joins);
}
}
impl JoinableElement for LogCard {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn as_element(&self) -> &(dyn Element + 'static) {
&*self
}
fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) {
&mut *self
}
fn set_borders(&mut self, borders: Borders) {
self.borders = borders;
}
fn set_joins(&mut self, joins: Borders) {
self.joins = joins;
}
}
impl InteractableElement for LogCard {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn as_element(&self) -> &(dyn Element + 'static) {
&*self
}
fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) {
&mut *self
}
fn interact(&mut self, key: KeyEvent) -> InteractionResult {
let entries = self.get_logs();
let estimated_width = 80usize;
let all_lines = self.build_all_lines(entries, estimated_width);
self.last_total_lines = all_lines.len();
let visible_height = self.last_visible_height.max(1);
match key.code {
KeyCode::Enter | KeyCode::Char(' ') => {
self.selected = !self.selected;
InteractionResult::Handled
}
KeyCode::Char('j') | KeyCode::Char('J') => {
let (can_up, _) = self.get_title_hints();
if can_up {
self.scroll_up();
}
InteractionResult::Handled
}
KeyCode::Char('k') | KeyCode::Char('K') => {
let (_, can_down) = self.get_title_hints();
if can_down {
self.scroll_down();
}
InteractionResult::Handled
}
KeyCode::Up if self.selected => {
let (can_up, _) = self.get_title_hints();
if can_up {
self.scroll_up();
}
InteractionResult::Handled
}
KeyCode::Down if self.selected => {
let (_, can_down) = self.get_title_hints();
if can_down {
self.scroll_down();
}
InteractionResult::Handled
}
KeyCode::Home => {
if self.last_total_lines > visible_height {
self.scroll_offset = self.last_total_lines - visible_height;
}
InteractionResult::Handled
}
KeyCode::End => {
self.scroll_offset = 0;
InteractionResult::Handled
}
_ => InteractionResult::Unhandled,
}
}
fn can_focus(&self) -> bool {
true
}
fn is_focused(&self) -> bool {
self.focused
}
fn focus(&mut self, f: bool) {
self.focused = f;
}
}

View file

@ -1,56 +0,0 @@
use crate::gui::ui::{UI, UNIQUE};
use crate::{RELOAD, SHUTDOWN};
use crossterm::event::{Event, KeyEvent, KeyEventKind, KeyModifiers, poll, read};
use std::sync::Arc;
use std::sync::atomic::Ordering;
use std::time::Duration;
pub fn setup_input_handler(ui: Arc<UI>) {
tokio::spawn(async move {
loop {
if *SHUTDOWN.read().await {
break;
}
let event_result = tokio::task::spawn_blocking(|| {
if let Ok(true) = poll(Duration::from_millis(100)) {
read().ok().and_then(|ev| match ev {
Event::Key(key) if key.kind == KeyEventKind::Press => Some(key),
_ => None,
})
} else {
None
}
})
.await;
match event_result {
Ok(Some(key_event)) => {
handle_input(key_event, ui.clone()).await;
UNIQUE.store(true, Ordering::Relaxed);
}
Ok(_) => {}
Err(e) => {
eprintln!("Input task error: {}", e);
tokio::time::sleep(Duration::from_millis(10)).await;
}
}
}
});
}
pub async fn handle_input(key: KeyEvent, ui: Arc<UI>) {
match (key.code, key.modifiers) {
(crossterm::event::KeyCode::Char('q'), KeyModifiers::CONTROL)
| (crossterm::event::KeyCode::Char('c'), KeyModifiers::CONTROL) => {
*SHUTDOWN.write().await = true;
}
(crossterm::event::KeyCode::Char('r'), KeyModifiers::CONTROL) => {
*RELOAD.write().await = true;
*SHUTDOWN.write().await = true;
}
_ => {
ui.handle_input(key).await;
}
}
}

View file

@ -1,49 +0,0 @@
use std::fmt::{Debug, Formatter};
use std::future::Future;
use std::pin::Pin;
use crate::gui::screens::screens::Screen;
#[allow(unused)]
pub enum InteractionResult {
CloseScreen,
OpenScreen {
screen: Box<dyn Screen>,
},
OpenFutureScreen {
screen: Pin<Box<dyn Future<Output = Box<dyn Screen>> + Send>>,
},
Handled,
Unhandled,
}
impl Debug for InteractionResult {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
InteractionResult::OpenScreen { screen: _ } => write!(f, "OpenScreen"),
InteractionResult::OpenFutureScreen { screen: _ } => write!(f, "OpenFutureScreen"),
InteractionResult::CloseScreen => write!(f, "CloseScreen"),
InteractionResult::Handled => write!(f, "Handled"),
InteractionResult::Unhandled => write!(f, "Unhandled"),
}
}
}
impl PartialEq for InteractionResult {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(
InteractionResult::OpenScreen { screen: _ },
InteractionResult::OpenScreen { screen: _ },
) => true,
(
InteractionResult::OpenFutureScreen { screen: _ },
InteractionResult::OpenFutureScreen { screen: _ },
) => true,
(InteractionResult::CloseScreen, InteractionResult::CloseScreen) => true,
(InteractionResult::Handled, InteractionResult::Handled) => true,
(InteractionResult::Unhandled, InteractionResult::Unhandled) => true,
_ => false,
}
}
}

View file

@ -1,20 +0,0 @@
pub mod elements {
pub mod console_card;
pub mod elements;
pub mod graph_card;
pub mod log_card;
}
pub mod screens {
pub mod main_screen;
pub mod md_viewer;
pub mod screens;
pub mod terms_checker;
pub mod terms_updater;
}
pub mod util {
pub mod borders;
}
pub mod app_state;
pub mod input_handler;
pub mod interaction_result;
pub mod ui;

View file

@ -1,242 +0,0 @@
use crate::gui::{
elements::{
console_card::ConsoleCard,
elements::{InteractableElement, JoinableElement},
graph_card::{GRAPHS, GraphCard},
log_card::LogCard,
},
interaction_result::InteractionResult,
screens::screens::{NavDirection, Screen},
ui::UI,
};
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{
Frame,
layout::{Constraint, Layout, Margin, Rect},
widgets::{Block, Borders},
};
use std::{any::Any, sync::Arc};
pub struct MainScreen {
elements: Vec<Box<dyn InteractableElement>>,
nav_grid: Vec<Vec<Option<usize>>>,
selected_coords: (usize, usize),
graphs_open: bool,
}
impl MainScreen {
pub async fn new(ui: Arc<UI>) -> Self {
let mut elements: Vec<Box<dyn InteractableElement>> = Vec::new();
let nav_grid = vec![
vec![Some(0), Some(2)],
vec![Some(0), Some(3)],
vec![Some(1), Some(4)],
];
let mut log_card = LogCard::new();
log_card.set_borders(Borders::TOP.union(Borders::RIGHT).union(Borders::LEFT));
let mut console_card = ConsoleCard::new("Console", "");
console_card.set_joins(Borders::TOP);
elements.push(Box::new(log_card));
elements.push(Box::new(console_card));
let mut ram_graph = GraphCard::new(ui.clone(), GRAPHS::Ram, "RAM".into());
ram_graph.set_borders(Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT));
elements.push(Box::new(ram_graph));
let mut cpu_graph = GraphCard::new(ui.clone(), GRAPHS::Cpu, "CPU".into());
cpu_graph.set_borders(Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT));
cpu_graph.set_joins(Borders::TOP);
elements.push(Box::new(cpu_graph));
let mut ping_graph = GraphCard::new(ui.clone(), GRAPHS::Ping, "Ping".into());
ping_graph.set_joins(Borders::TOP);
elements.push(Box::new(ping_graph));
let graphs_open = true;
let mut screen = MainScreen {
elements,
nav_grid,
selected_coords: (1, 0),
graphs_open,
};
screen.focus_current();
screen
}
fn focus_current(&mut self) {
let (y, x) = self.selected_coords;
if let Some(Some(index)) = self.nav_grid.get(y).and_then(|row| row.get(x)) {
if let Some(element) = self.elements.get_mut(*index) {
if element.can_focus() {
element.focus(true);
}
}
}
}
fn unfocus_current(&mut self, y: usize, x: usize) {
if let Some(Some(index)) = self.nav_grid.get(y).and_then(|row| row.get(x)) {
if let Some(element) = self.elements.get_mut(*index) {
element.focus(false);
}
}
}
fn navigate(&mut self, direction: NavDirection) {
let (current_row, current_col) = self.selected_coords;
let current_element = self.nav_grid[current_row][current_col];
self.unfocus_current(current_row, current_col);
let (delta_row, delta_col) = match direction {
NavDirection::Up => (-1isize, 0),
NavDirection::Down => (1, 0),
NavDirection::Left => (0, -1),
NavDirection::Right => (0, 1),
_ => (0, 0),
};
let mut next_row = current_row as isize;
let mut next_col = current_col as isize;
loop {
next_row += delta_row;
next_col += delta_col;
if next_row < 0 || next_col < 0 {
self.selected_coords = (
(next_row - delta_row) as usize,
(next_col - delta_col) as usize,
);
break;
}
let next_row_u = next_row as usize;
let next_col_u = next_col as usize;
if next_row_u >= self.nav_grid.len() {
self.selected_coords = (
(next_row - delta_row) as usize,
(next_col - delta_col) as usize,
);
break;
}
if let Some(row) = self.nav_grid.get(next_row_u) {
if next_col_u >= row.len() {
self.selected_coords = (
(next_row - delta_row) as usize,
(next_col - delta_col) as usize,
);
break;
}
if let Some(next_element) = row[next_col_u] {
if Some(next_element) != current_element {
self.selected_coords = (next_row_u, next_col_u);
self.focus_current();
return;
}
}
}
}
self.focus_current();
}
}
impl Screen for MainScreen {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn render(&self, f: &mut Frame, rect: Rect) {
let main_block = Block::default().title("Main").borders(Borders::ALL);
f.render_widget(main_block, rect);
let inner = rect.inner(Margin {
vertical: 1,
horizontal: 1,
});
let graphs_width = if self.graphs_open { 30 } else { 2 };
let main_width = inner.width.saturating_sub(graphs_width);
let horizontal_chunks = Layout::default()
.direction(ratatui::layout::Direction::Horizontal)
.constraints([
Constraint::Length(main_width),
Constraint::Length(graphs_width),
])
.split(inner);
let left_area = horizontal_chunks[0];
let right_area = horizontal_chunks[1];
let left_rows =
Layout::vertical([Constraint::Min(0), Constraint::Length(3)]).split(left_area);
if let Some(log) = self.elements.get(0) {
log.as_element().render(f, left_rows[0]);
}
if let Some(console) = self.elements.get(1) {
console.as_element().render(f, left_rows[1]);
}
let graph_elements: Vec<_> = self
.elements
.iter()
.filter(|el| el.as_any().is::<GraphCard>())
.collect();
if !graph_elements.is_empty() {
let graph_chunks = Layout::vertical(
graph_elements
.iter()
.map(|_| Constraint::Ratio(1, graph_elements.len() as u32))
.collect::<Vec<_>>(),
)
.split(right_area);
for (el, area) in graph_elements.iter().zip(graph_chunks.iter()) {
el.as_element().render(f, *area);
}
}
}
fn handle_input(&mut self, event: KeyEvent) -> InteractionResult {
match event.code {
KeyCode::Up => self.navigate(NavDirection::Up),
KeyCode::Down => self.navigate(NavDirection::Down),
KeyCode::Left => self.navigate(NavDirection::Left),
KeyCode::Right => self.navigate(NavDirection::Right),
KeyCode::Enter | KeyCode::Char(' ') if self.selected_coords.1 == 1 => {
self.graphs_open = !self.graphs_open;
for element in self.elements.iter_mut() {
if let Some(graph) = element.as_any_mut().downcast_mut::<GraphCard>() {
graph.set_open(self.graphs_open);
}
}
return InteractionResult::Handled;
}
_ => {
let (y, x) = self.selected_coords;
if let Some(Some(index)) = self.nav_grid.get(y).and_then(|r| r.get(x)) {
if let Some(el) = self.elements.get_mut(*index) {
return el.interact(event);
}
}
}
}
InteractionResult::Handled
}
}

View file

@ -1,493 +0,0 @@
use crossterm::event::{self, Event, KeyCode, KeyEvent};
use ratatui::{
DefaultTerminal,
prelude::*,
text::{Line, Span},
widgets::{Block, Borders, Paragraph, Wrap},
};
use std::{any::Any, time::Duration};
use crate::gui::{interaction_result::InteractionResult, screens::screens::Screen};
pub struct FileViewer {
title: String,
text: Vec<DisplayLine>,
scroll: u16,
scroll_x: u16,
}
impl Screen for FileViewer {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn render(&self, f: &mut Frame, rect: Rect) {
self.draw(f, rect);
}
fn handle_input(&mut self, event: KeyEvent) -> InteractionResult {
match event.code {
KeyCode::Char('q') | KeyCode::Esc => {
return InteractionResult::CloseScreen;
}
KeyCode::Down => self.scroll = self.scroll.saturating_add(1),
KeyCode::Up => self.scroll = self.scroll.saturating_sub(1),
KeyCode::PageDown => self.scroll = self.scroll.saturating_add(10),
KeyCode::PageUp => self.scroll = self.scroll.saturating_sub(10),
KeyCode::Right => self.scroll_x = self.scroll_x.saturating_add(2),
KeyCode::Left => self.scroll_x = self.scroll_x.saturating_sub(2),
_ => {}
}
InteractionResult::Unhandled
}
}
impl FileViewer {
pub fn new(title: String, content: &str) -> Self {
Self {
title,
text: parse_document(content.to_owned()),
scroll: 0,
scroll_x: 0,
}
}
pub fn force_popup(mut self, mut terminal: DefaultTerminal) -> DefaultTerminal {
loop {
terminal
.draw(|f| {
let area = f.area();
self.draw(f, area);
})
.unwrap();
if event::poll(Duration::from_millis(100)).unwrap() {
let ev = event::read().unwrap();
self.handle_event(&ev);
if matches!(ev, Event::Key(k) if k.code == KeyCode::Char('q')) {
break;
}
}
}
terminal
}
fn draw(&self, f: &mut Frame, area: Rect) {
use ratatui::text::Text;
let mut rendered_lines = Vec::new();
for display_line in &self.text {
if display_line.scrollable {
let content: String = display_line
.line
.spans
.iter()
.map(|s| s.content.clone())
.collect();
let start = self.scroll_x as usize;
let width = area.width as usize - 2;
let visible = if start < content.chars().count() {
content.chars().skip(start).take(width).collect()
} else {
String::new()
};
let mut chars: Vec<char> = visible.chars().collect();
if start > 0 && !chars.is_empty() {
chars[0] = '<';
}
if start + width < content.chars().count() && !chars.is_empty() {
let last = chars.len() - 1;
chars[last] = '>';
}
let visible: String = chars.into_iter().collect();
rendered_lines.push(Line::from(Span::styled(
visible,
display_line
.line
.spans
.first()
.map(|s| s.style)
.unwrap_or_default(),
)));
} else {
rendered_lines.push(display_line.line.clone());
}
}
let paragraph = Paragraph::new(Text::from(rendered_lines))
.block(
Block::default()
.borders(Borders::ALL)
.title(format!("{} - [Q to close]", self.title.as_str(),)),
)
.wrap(Wrap { trim: false })
.scroll((self.scroll, 0));
f.render_widget(paragraph, area);
}
pub fn handle_event(&mut self, event: &Event) {
if let Event::Key(key) = event {
match key.code {
KeyCode::Down => self.scroll = self.scroll.saturating_add(1),
KeyCode::Up => self.scroll = self.scroll.saturating_sub(1),
KeyCode::PageDown => self.scroll = self.scroll.saturating_add(10),
KeyCode::PageUp => self.scroll = self.scroll.saturating_sub(10),
KeyCode::Right => self.scroll_x = self.scroll_x.saturating_add(2),
KeyCode::Left => self.scroll_x = self.scroll_x.saturating_sub(2),
_ => {}
}
}
}
}
fn parse_document(input: String) -> Vec<DisplayLine> {
let mut lines_vec = Vec::new();
let mut in_code_block = false;
let liness: Vec<String> = input.lines().map(String::from).collect();
let mut i = 0;
while i < liness.len() {
let raw = &liness[i];
if raw.trim().starts_with("```") {
in_code_block = !in_code_block;
let code: String = if raw.trim().replace("```", "").is_empty() {
"──".to_string()
} else {
raw.trim().replace("```", "")
};
lines_vec.push(DisplayLine {
line: Line::from(Span::styled(
format!("────────{}────────", code),
Style::default().fg(Color::DarkGray),
)),
scrollable: false,
});
i += 1;
continue;
}
if in_code_block {
lines_vec.push(DisplayLine {
line: Line::from(Span::styled(
raw.to_string(),
Style::default().fg(Color::Yellow),
)),
scrollable: false,
});
i += 1;
continue;
}
if raw.starts_with("### ") {
lines_vec.push(DisplayLine {
line: Line::from(Span::styled(
raw.trim_start_matches("### ").to_string(),
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)),
scrollable: false,
});
i += 1;
continue;
}
if raw.starts_with("## ") {
lines_vec.push(DisplayLine {
line: Line::from(Span::styled(
raw.trim_start_matches("## ").to_string(),
Style::default()
.fg(Color::LightCyan)
.add_modifier(Modifier::BOLD),
)),
scrollable: false,
});
i += 1;
continue;
}
if raw.starts_with("# ") {
lines_vec.push(DisplayLine {
line: Line::from(Span::styled(
raw.trim_start_matches("# ").to_string(),
Style::default()
.fg(Color::Gray)
.add_modifier(Modifier::BOLD),
)),
scrollable: false,
});
i += 1;
continue;
}
if raw.trim_start().starts_with("- ") {
let indent = raw.chars().take_while(|c| *c == ' ').count();
lines_vec.push(DisplayLine {
line: Line::from(Span::raw(format!(
"{}• {}",
" ".repeat(indent),
raw.trim_start_matches("- ")
))),
scrollable: false,
});
i += 1;
continue;
}
if raw.trim().starts_with('|') && raw.contains('|') {
let mut table_lines = vec![raw.clone()];
let mut j = i + 1;
while j < liness.len() && liness[j].trim().starts_with('|') {
table_lines.push(liness[j].clone());
j += 1;
}
let table = parse_table(&table_lines.iter().map(|s| s.as_str()).collect::<Vec<_>>());
lines_vec.extend(table_to_lines(table));
i = j;
continue;
}
lines_vec.push(DisplayLine {
line: Line::from(parse_inline(raw.as_str())),
scrollable: false,
});
i += 1;
}
lines_vec
}
fn parse_inline(input: &str) -> Vec<Span<'static>> {
let mut spans = Vec::new();
let mut buf = String::new();
let mut bold = false;
let mut underline = false;
let mut code = false;
let mut chars = input.chars().peekable();
while let Some(c) = chars.next() {
let toggle = match c {
'*' if chars.peek() == Some(&'*') => {
chars.next();
Some("bold")
}
'_' if chars.peek() == Some(&'_') => {
chars.next();
Some("underline")
}
'`' => Some("code"),
_ => None,
};
if let Some(kind) = toggle {
flush_span(&mut spans, &mut buf, current_style(bold, underline, code));
match kind {
"bold" => bold = !bold,
"underline" => underline = !underline,
"code" => code = !code,
_ => {}
}
continue;
}
buf.push(c);
}
flush_span(&mut spans, &mut buf, current_style(bold, underline, code));
spans
}
fn current_style(bold: bool, underline: bool, code: bool) -> Style {
let mut style = Style::default();
if bold {
style = style.add_modifier(Modifier::BOLD);
}
if underline {
style = style.add_modifier(Modifier::UNDERLINED);
}
if code {
style = style.fg(Color::Yellow);
}
style
}
#[derive(Clone)]
pub struct DisplayLine {
line: Line<'static>,
scrollable: bool,
}
fn table_to_lines(table: Vec<Vec<String>>) -> Vec<DisplayLine> {
if table.len() < 2 {
return vec![];
}
let header = &table[0];
let mut column_heights = vec![0; table[0].len()];
for row in table.iter().skip(1) {
for (i, cell) in row.iter().enumerate() {
let lines = cell.lines().count().max(1);
column_heights[i] += lines;
}
}
let widths: Vec<usize> = header
.iter()
.enumerate()
.map(|(i, h)| {
let h_len = h.chars().count().max(1);
if i == 0 {
table
.iter()
.map(|row| row.get(i).map(|c| c.chars().count()).unwrap_or(0))
.max()
.unwrap_or(h_len)
} else {
let max = (3 * h_len) as usize;
max.max(h_len)
}
})
.collect();
let mut lines = Vec::new();
for (row_idx, row) in table.iter().enumerate() {
if row_idx == 1 {
let divider = widths
.iter()
.map(|w| "".repeat(*w))
.collect::<Vec<_>>()
.join("─┼─");
lines.push(DisplayLine {
line: Line::from(Span::styled(divider, Style::default().fg(Color::DarkGray))),
scrollable: true,
});
continue;
}
let wrapped_cells: Vec<Vec<String>> = row
.iter()
.enumerate()
.map(|(i, cell)| wrap_cell(cell, widths[i]))
.collect();
let row_height = wrapped_cells.iter().map(|c| c.len()).max().unwrap_or(1);
for line_idx in 0..row_height {
let mut line = String::new();
for (i, cell) in wrapped_cells.iter().enumerate() {
let content = cell.get(line_idx).map(String::as_str).unwrap_or("");
line.push_str(&format!("{:width$}", content, width = widths[i]));
if i < wrapped_cells.len() - 1 {
line.push_str("");
}
}
let style = if row_idx == 0 {
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::Green)
};
lines.push(DisplayLine {
line: Line::from(Span::styled(line, style)),
scrollable: true,
});
}
}
lines
}
fn wrap_cell(cell: &str, width: usize) -> Vec<String> {
if width == 0 {
return vec![String::new()];
}
let mut lines = Vec::new();
let mut current = String::new();
for word in cell.split_whitespace() {
let word_len = word.chars().count();
let current_len = current.chars().count();
if current_len == 0 {
if word_len <= width {
current.push_str(word);
} else {
for chunk in word.chars().collect::<Vec<_>>().chunks(width) {
lines.push(chunk.iter().collect());
}
}
} else if current_len + 1 + word_len <= width {
current.push(' ');
current.push_str(word);
} else {
lines.push(current);
current = String::new();
if word_len <= width {
current.push_str(word);
} else {
for chunk in word.chars().collect::<Vec<_>>().chunks(width) {
lines.push(chunk.iter().collect());
}
}
}
}
if !current.is_empty() {
lines.push(current);
}
if lines.is_empty() {
lines.push(String::new());
}
lines
}
fn flush_span(spans: &mut Vec<Span>, buf: &mut String, style: Style) {
if !buf.is_empty() {
spans.push(Span::styled(buf.clone(), style));
buf.clear();
}
}
fn parse_table(lines: &[&str]) -> Vec<Vec<String>> {
let mut table = Vec::new();
for &line in lines {
if !line.starts_with('|') || !line.contains('|') {
break;
}
let row: Vec<String> = line
.trim_matches('|')
.split('|')
.map(|s| s.trim().to_string())
.collect();
table.push(row);
}
table
}

View file

@ -1,25 +0,0 @@
use std::any::Any;
use crossterm::event::KeyEvent;
use ratatui::{Frame, layout::Rect};
use crate::gui::interaction_result::InteractionResult;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NavDirection {
Up,
Down,
Left,
Right,
Next,
Prev,
}
pub trait Screen: Send + Sync + Any {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn render(&self, f: &mut Frame, rect: Rect);
fn handle_input(&mut self, event: KeyEvent) -> InteractionResult;
}

View file

@ -1,351 +0,0 @@
use crate::{
gui::{
interaction_result::InteractionResult,
screens::{md_viewer::FileViewer, screens::Screen},
ui::UI,
},
terms::{
buttons::{checkbox, draw_buttons},
consent_state::UserChoice,
focus::Focus,
terms_getter::{Type, get_link, get_terms},
},
};
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{
Frame,
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Style},
text::{Line, Span, Text},
widgets::{Block, Borders, Paragraph},
};
use std::{any::Any, pin::Pin, sync::Arc};
use tokio::sync::oneshot;
pub struct TermsCheckerScreen {
ui: Arc<UI>,
sender: Option<oneshot::Sender<UserChoice>>,
eula: bool,
tos: bool,
pp: bool,
focus: Focus,
}
impl TermsCheckerScreen {
pub fn new(ui: Arc<UI>, sender: Option<oneshot::Sender<UserChoice>>) -> Self {
Self {
ui,
sender,
eula: false,
tos: false,
pp: false,
focus: Focus::Eula,
}
}
}
impl Screen for TermsCheckerScreen {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn render(&self, f: &mut Frame, size: Rect) {
let mut needed_height = 5;
if size.height < 6 || size.width < 27 {
f.render_widget(
Line::from(format!("too small {}/63 by {}/13", size.width, size.height)),
size,
);
return;
}
let max_width = 150;
let max_height = 26;
let content_width = if max_width < size.width {
max_width
} else {
size.width
};
let content_height = if max_height < size.height {
max_height
} else {
size.height
};
let horizontal_margin = (size.width.saturating_sub(content_width)) / 2;
let vertical_margin = (size.height.saturating_sub(content_height)) / 2;
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(7), Constraint::Length(3)])
.split(Rect {
x: horizontal_margin,
y: vertical_margin,
width: content_width,
height: content_height,
});
let eula_text = if size.width < 70 {
"EULA ¹ (https://legal.tensamin.net/eula/)"
} else {
"End User Licence Agreement ¹ (https://legal.tensamin.net/eula/)"
};
let tos_text = if size.width < 72 {
"ToS ² (https://legal.tensamin.net/terms-of-service/)"
} else {
"Terms of Service ² (https://legal.tensamin.net/terms-of-service/)"
};
let pp_text = if size.width < 68 {
"PP ² (https://legal.tensamin.net/privacy-policy/)"
} else {
"Privacy Policy ² (https://legal.tensamin.net/privacy-policy/)"
};
let (mut optional_lines, agree_lines): (Vec<i16>, Vec<&str>) = if size.width > 143 {
(
vec![8, 3, 8, 5],
vec![
"",
"By selecting Continue, you confirm that you agree to the End User License Agreement and applicable Terms of Service.",
"",
"Tensamin services require acceptance of the Terms of Service and Privacy Policy.",
"",
"While having a document selected press O to view in this UI or press L to open as a link.",
],
)
} else if size.width > 92 {
(
vec![9, 3, 9, 5],
vec![
"",
"By selecting Continue, you confirm that you agree to the End User License",
"Agreement and applicable Terms of Service.",
"",
"Tensamin services require acceptance of the Terms of Service and Privacy Policy.",
"",
"While having a document selected press O to view in this UI or press L to open as a link.",
],
)
} else if size.width > 73 {
(
vec![9, 3, 9, 5],
vec![
"",
"By selecting Continue, you confirm that you agree to the",
"End User License Agreement and applicable Terms of Service.",
"",
"Tensamin services require acceptance of the ToS and Privacy Policy.",
"",
"While having a document selected press O to view in this UI or press L",
"to open as a link.",
],
)
} else {
(
vec![10, 3, 11, 5],
vec![
"",
"By selecting Continue, you confirm that you agree",
"to the End User License Agreement and",
"applicable Terms of Service.",
"",
"Tensamin services require acceptance of the",
"Terms of Service and Privacy Policy.",
"",
"While having a document selected press O to view",
"in this UI or press L to open as a link.",
],
)
};
let mut text_lines = vec![
checkbox(eula_text, self.eula, self.focus == Focus::Eula, true),
checkbox(tos_text, self.tos, self.focus == Focus::Tos, self.eula),
checkbox(pp_text, self.pp, self.focus == Focus::Pp, self.eula),
Line::from(""),
Line::from("¹ Necessary required to run the program"),
Line::from("² Optional required only for Tensamin services"),
];
for line in agree_lines {
text_lines.insert(text_lines.len(), Line::from(line));
}
while size.height - 5 < text_lines.len() as u16 {
if optional_lines.len() == 0 {
break;
}
text_lines.remove(optional_lines[0] as usize);
optional_lines.remove(0);
}
needed_height += text_lines.len();
if size.width < 60 || size.height < needed_height as u16 {
let width_style = if size.width > 76 {
Style::default().fg(Color::Green)
} else if size.width >= 60 {
Style::default().fg(Color::Yellow)
} else {
Style::default().fg(Color::Red)
};
let height_style = if size.height > 19 {
Style::default().fg(Color::Green)
} else if size.height >= 13 {
Style::default().fg(Color::Yellow)
} else {
Style::default().fg(Color::Red)
};
let warning_text = Text::from(vec![
Line::from(vec![
Span::raw("Width: "),
Span::styled(format!("{}", size.width), width_style),
Span::raw(" / 60"),
]),
Line::from(vec![
Span::raw("Height: "),
Span::styled(format!("{}", size.height), height_style),
Span::raw(format!(" / 13")),
]),
]);
let warning = Paragraph::new(warning_text)
.alignment(Alignment::Center)
.block(
Block::default()
.borders(Borders::ALL)
.title(format!("UI Too Small [Q to Quit]")),
);
f.render_widget(warning, size);
return;
}
let consent_block = Paragraph::new(Text::from(text_lines)).block(
Block::default()
.title(format!(" Tensamin User Consent [Q to Quit] ",))
.borders(Borders::ALL),
);
f.render_widget(consent_block, chunks[0]);
draw_buttons(
f,
chunks[1],
self.focus,
(self.eula, self.tos && self.pp),
true,
false,
true,
);
}
fn handle_input(&mut self, event: KeyEvent) -> InteractionResult {
let mut possible_states = vec![Focus::Eula, Focus::Tos, Focus::Pp, Focus::Cancel];
if self.eula {
possible_states.push(Focus::Continue);
if self.tos && self.pp {
possible_states.push(Focus::ContinueAll);
}
}
match event.code {
KeyCode::Esc => {
if let Some(sender) = self.sender.take() {
let _ = sender.send(UserChoice::Deny);
}
InteractionResult::CloseScreen
}
KeyCode::Up | KeyCode::Left => {
self.focus.prev(&possible_states);
InteractionResult::Handled
}
KeyCode::Down | KeyCode::Right | KeyCode::Tab => {
self.focus.next(&possible_states);
InteractionResult::Handled
}
KeyCode::Char('q') | KeyCode::Char('Q') => {
if let Some(sender) = self.sender.take() {
let _ = sender.send(UserChoice::Deny);
}
InteractionResult::CloseScreen
}
KeyCode::Char('o') | KeyCode::Char('O') => {
let terms_type = match self.focus {
Focus::Eula => Some(Type::EULA),
Focus::Tos => Some(Type::TOS),
Focus::Pp => Some(Type::PP),
_ => None,
};
if let Some(terms_type) = terms_type {
let fut: Pin<Box<dyn Future<Output = Box<dyn Screen>> + Send>> =
Box::pin(async move {
let content = get_terms(terms_type.clone()).await.unwrap();
let screen: FileViewer =
FileViewer::new(terms_type.to_string(), &content);
Box::new(screen) as Box<dyn Screen>
});
InteractionResult::OpenFutureScreen { screen: fut }
} else {
InteractionResult::Unhandled
}
}
KeyCode::Char('l') | KeyCode::Char('L') => match self.focus {
Focus::Eula => {
let _ = open::that(get_link(Type::EULA));
InteractionResult::Handled
}
Focus::Tos => {
let _ = open::that(get_link(Type::TOS));
InteractionResult::Handled
}
Focus::Pp => {
let _ = open::that(get_link(Type::PP));
InteractionResult::Handled
}
_ => InteractionResult::Unhandled,
},
KeyCode::Enter | KeyCode::Char(' ') => match self.focus {
Focus::Eula => {
self.eula = !self.eula;
self.tos = false;
self.pp = false;
InteractionResult::Handled
}
Focus::Tos if self.eula => {
self.tos = !self.tos;
InteractionResult::Handled
}
Focus::Pp if self.eula => {
self.pp = !self.pp;
InteractionResult::Handled
}
Focus::Cancel => {
if let Some(sender) = self.sender.take() {
let _ = sender.send(UserChoice::Deny);
}
InteractionResult::CloseScreen
}
Focus::Continue if self.eula => {
if let Some(sender) = self.sender.take() {
let _ = sender.send(UserChoice::AcceptEULA);
}
InteractionResult::CloseScreen
}
Focus::ContinueAll if self.eula && self.tos && self.pp => {
if let Some(sender) = self.sender.take() {
let _ = sender.send(UserChoice::AcceptAll);
}
InteractionResult::CloseScreen
}
_ => InteractionResult::Unhandled,
},
_ => InteractionResult::Unhandled,
}
}
}

View file

@ -1,691 +0,0 @@
use crate::{
gui::{
interaction_result::InteractionResult,
screens::{md_viewer::FileViewer, screens::Screen},
},
terms::{
buttons::{checkbox, draw_buttons},
consent_state::{UpdateDecision, UserChoice},
doc::Doc,
focus::Focus,
terms_getter::{Type, get_newest_link, get_terms},
},
};
use chrono::{Local, TimeZone, Utc};
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{
Frame,
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Style},
text::{Line, Span, Text},
widgets::{Block, Borders, Paragraph},
};
use std::any::Any;
use tokio::sync::oneshot;
pub struct TermsUpdaterScreen {
sender: Option<oneshot::Sender<UserChoice>>,
eula_needed: bool,
tos_needed: bool,
pp_needed: bool,
eula_future: bool,
tos_future: bool,
pp_future: bool,
eula_for_future: Option<Doc>,
tos_for_future: Option<Doc>,
pp_for_future: Option<Doc>,
update_needed: bool,
eula: bool,
tos: bool,
pp: bool,
focus: Focus,
}
impl TermsUpdaterScreen {
pub fn new(
consent_eula: UpdateDecision,
consent_tos: UpdateDecision,
consent_pp: UpdateDecision,
sender: Option<oneshot::Sender<UserChoice>>,
) -> Self {
let (eula_needed, eula_future, eula_for_future): (bool, bool, Option<Doc>) =
match consent_eula {
UpdateDecision::NoChange => (false, false, None),
UpdateDecision::Future { newest } => (true, true, Some(newest)),
UpdateDecision::Forced(doc) => (true, false, Some(doc)),
};
let (tos_needed, tos_future, tos_for_future): (bool, bool, Option<Doc>) = match consent_tos
{
UpdateDecision::NoChange => (false, false, None),
UpdateDecision::Future { newest } => (true, true, Some(newest)),
UpdateDecision::Forced(doc) => (true, false, Some(doc)),
};
let (pp_needed, pp_future, pp_for_future): (bool, bool, Option<Doc>) = match consent_pp {
UpdateDecision::NoChange => (false, false, None),
UpdateDecision::Future { newest } => (true, true, Some(newest)),
UpdateDecision::Forced(doc) => (true, false, Some(doc)),
};
let focus = if eula_needed {
Focus::Eula
} else if tos_needed {
Focus::Tos
} else if pp_needed {
Focus::Pp
} else {
Focus::Cancel
};
let update_needed = (eula_needed && !eula_future)
|| (tos_needed && !tos_future)
|| (pp_needed && !pp_future);
Self {
sender,
eula_needed,
tos_needed,
pp_needed,
eula_for_future,
tos_for_future,
pp_for_future,
eula_future,
tos_future,
pp_future,
update_needed,
eula: !eula_needed,
tos: !tos_needed,
pp: !pp_needed,
focus,
}
}
}
impl Screen for TermsUpdaterScreen {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn render(&self, f: &mut Frame, size: Rect) {
let mut needed_height = 5;
if size.height < 6 || size.width < 27 {
f.render_widget(
Line::from(format!("too small {}/63 by {}/15", size.width, size.height)),
size,
);
return;
}
let max_width = 150;
let max_height = 30;
let content_width = if max_width < size.width {
max_width
} else {
size.width
};
let content_height = if max_height < size.height {
max_height
} else {
size.height
};
let horizontal_margin = (size.width.saturating_sub(content_width)) / 2;
let vertical_margin = (size.height.saturating_sub(content_height)) / 2;
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(7), Constraint::Length(3)])
.split(Rect {
x: horizontal_margin,
y: vertical_margin,
width: content_width,
height: content_height,
});
let mut text_lines: Vec<Line> = Vec::new();
let mut header_lines = 0;
let separator = if size.width > 72 {
header_lines += 3;
text_lines.push(Line::from(
"You previously accepted earlier versions of Tensamins legal documents.",
));
text_lines.push(Line::from(
"Some of them have been updated and are listed below for your review.",
));
text_lines.push(Line::from(""));
2
} else {
header_lines += 5;
text_lines.push(Line::from("You previously accepted earlier "));
text_lines.push(Line::from("versions of Tensamins legal documents."));
text_lines.push(Line::from("Some of them have been updated and"));
text_lines.push(Line::from("are listed below for your review."));
text_lines.push(Line::from(""));
4
};
if self.eula_needed {
header_lines += 1;
if self.eula_future {
if size.width < 80 {
text_lines.push(checkbox(
"EULA ¹³ (https://legal.tensamin.net/eula/newest/)",
self.eula,
self.focus == Focus::Eula,
true,
));
header_lines += 1;
let unix_timestamp = self.eula_for_future.clone().unwrap().get_time() as i64;
let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap();
let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M");
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
} else {
text_lines.push(checkbox(
"End User Licence Agreement ¹³ (https://legal.tensamin.net/eula/newest/)",
self.eula,
self.focus == Focus::Eula,
true,
));
header_lines += 1;
let unix_timestamp = self.eula_for_future.clone().unwrap().get_time() as i64;
let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap();
let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M");
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
}
} else {
if size.width < 80 {
text_lines.push(checkbox(
"EULA ¹ (https://legal.tensamin.net/eula/newest/)",
self.eula,
self.focus == Focus::Eula,
true,
));
} else {
text_lines.push(checkbox(
"End User Licence Agreement ¹ (https://legal.tensamin.net/eula/newest/)",
self.eula,
self.focus == Focus::Eula,
true,
));
}
}
}
if self.tos_needed {
header_lines += 1;
if self.tos_future {
if size.width < 80 {
text_lines.push(checkbox(
"ToS ²³ (https://legal.tensamin.net/tos/newest/)",
self.tos,
self.focus == Focus::Tos,
self.eula,
));
header_lines += 1;
let unix_timestamp = self.tos_for_future.clone().unwrap().get_time() as i64;
let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap();
let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M");
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
} else {
text_lines.push(checkbox(
"Terms of Service ²³ (https://legal.tensamin.net/terms-of-service/newest/)",
self.tos,
self.focus == Focus::Tos,
self.eula,
));
header_lines += 1;
let unix_timestamp = self.tos_for_future.clone().unwrap().get_time() as i64;
let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap();
let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M");
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
}
} else {
if size.width < 80 {
text_lines.push(checkbox(
"ToS ² (https://legal.tensamin.net/tos/newest/)",
self.tos,
self.focus == Focus::Tos,
self.eula,
));
} else {
text_lines.push(checkbox(
"Terms of Service ² (https://legal.tensamin.net/terms-of-service/newest/)",
self.tos,
self.focus == Focus::Tos,
self.eula,
));
}
}
}
if self.pp_needed {
header_lines += 1;
if self.pp_future {
if size.width < 80 {
text_lines.push(checkbox(
"PP ²³ (https://legal.tensamin.net/privacy-policy/newest/)",
self.pp,
self.focus == Focus::Pp,
self.eula,
));
header_lines += 1;
let unix_timestamp = self.pp_for_future.clone().unwrap().get_time() as i64;
let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap();
let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M");
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
} else {
text_lines.push(checkbox(
"Privacy Policy ²³ (https://legal.tensamin.net/privacy-policy/newest/)",
self.pp,
self.focus == Focus::Pp,
self.eula,
));
header_lines += 1;
let unix_timestamp = self.pp_for_future.clone().unwrap().get_time() as i64;
let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap();
let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M");
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
}
} else {
if size.width < 80 {
text_lines.push(checkbox(
"PP ² (https://legal.tensamin.net/privacy-policy/newest/)",
self.pp,
self.focus == Focus::Pp,
self.eula,
));
} else {
text_lines.push(checkbox(
"Privacy Policy ² (https://legal.tensamin.net/privacy-policy/newest/)",
self.pp,
self.focus == Focus::Pp,
self.eula,
));
}
}
}
text_lines.push(Line::from(""));
text_lines.push(Line::from("¹ Necessary to run the program"));
text_lines.push(Line::from("² Optional - only for Tensamin services"));
if size.width < 100 {
text_lines.push(Line::from(
"³ Future version - consent stored now, takes effect later",
));
} else {
text_lines.push(Line::from("³ Future version - Youll continue using this version, automatically updated when changes apply."));
}
text_lines.push(Line::from(""));
let mut optional_lines: Vec<i16> = if size.width > 143 {
if self.tos_needed || self.pp_needed {
text_lines.push(Line::from("By selecting Downgrade, you confirm that you agree to the End User License Agreement and applicable Terms of Service."));
text_lines.push(Line::from(
"On Downgrade: Tensamin Services will deactivate once these changes apply.",
));
} else {
text_lines.push(Line::from("By selecting Continue, you confirm that you agree to the End User License Agreement and applicable Terms of Service."));
}
text_lines.push(Line::from(""));
text_lines.push(Line::from(
"Tensamin services require acceptance of the Terms of Service and Privacy Policy.",
));
text_lines.push(Line::from(""));
text_lines.push(Line::from("While having a document selected press O to view in this UI or press L to open as a link."));
if self.tos_needed || self.pp_needed {
vec![
header_lines + 7,
header_lines,
header_lines + 7,
separator,
header_lines + 2,
]
} else {
vec![
header_lines + 6,
header_lines,
header_lines + 6,
separator,
header_lines + 2,
]
}
} else if size.width > 92 {
if self.tos_needed || self.pp_needed {
text_lines.push(Line::from(
"By selecting Continue, you confirm that you agree to the End User",
));
text_lines.push(Line::from(
"License Agreement and applicable Terms of Service.",
));
text_lines.push(Line::from(
"On Downgrade: Tensamin Services will deactivate once these changes apply.",
));
} else {
text_lines.push(Line::from(
"By selecting Continue, you confirm that you agree to the End User",
));
text_lines.push(Line::from(
"License Agreement and applicable Terms of Service.",
));
}
text_lines.push(Line::from(""));
text_lines.push(Line::from(
"Tensamin services require acceptance of the Terms of Service and Privacy Policy.",
));
text_lines.push(Line::from(""));
text_lines.push(Line::from("While having a document selected press O to view in this UI or press L to open as a link."));
if self.tos_needed || self.pp_needed {
vec![
header_lines + 8,
header_lines,
header_lines + 8,
separator,
header_lines + 2,
]
} else {
vec![
header_lines + 7,
header_lines,
header_lines + 7,
separator,
header_lines + 2,
]
}
} else if size.width > 73 {
if self.tos_needed || self.pp_needed {
text_lines.push(Line::from(
"By selecting Continue, you confirm that you read, understood and",
));
text_lines.push(Line::from(
"agree to the End User License Agreement and applicable Terms of Service.",
));
text_lines.push(Line::from(
"On Downgrade: Tensamin Services will deactivate once these changes apply.",
));
} else {
text_lines.push(Line::from(
"By selecting Continue, you confirm that you read, understood and",
));
text_lines.push(Line::from(
"agree to the End User License Agreement and applicable Terms of Service.",
));
}
text_lines.push(Line::from(""));
text_lines.push(Line::from(
"Tensamin services require acceptance of the ToS and Privacy Policy.",
));
text_lines.push(Line::from(""));
text_lines.push(Line::from(
"While having a document selected press O to view in this UI or press L",
));
text_lines.push(Line::from("to open as a link."));
if self.tos_needed || self.pp_needed {
vec![
header_lines + 8,
header_lines,
header_lines + 8,
separator,
header_lines + 2,
]
} else {
vec![
header_lines + 7,
header_lines,
header_lines + 7,
separator,
header_lines + 2,
]
}
} else {
if self.tos_needed || self.pp_needed {
text_lines.push(Line::from("By selecting Continue, you confirm that you"));
text_lines.push(Line::from("agree to the End User License"));
text_lines.push(Line::from("Agreement and applicable Terms of Service."));
text_lines.push(Line::from(
"On Downgrade: Tensamin Services will deactivate",
));
text_lines.push(Line::from("once these changes apply."));
} else {
text_lines.push(Line::from("By selecting Continue, you confirm that you"));
text_lines.push(Line::from("agree to the End User License"));
text_lines.push(Line::from("Agreement and applicable Terms of Service."));
}
text_lines.push(Line::from(""));
text_lines.push(Line::from("Tensamin services require acceptance of the"));
text_lines.push(Line::from("Terms of Service and Privacy Policy."));
text_lines.push(Line::from(""));
text_lines.push(Line::from(
"While having a document selected press O to view",
));
text_lines.push(Line::from("in this UI or press L to open as a link."));
if self.tos_needed || self.pp_needed {
vec![
header_lines + 10,
header_lines,
header_lines + 11,
separator,
header_lines + 2,
]
} else {
vec![
header_lines + 8,
header_lines,
header_lines + 9,
separator,
header_lines + 2,
]
}
};
while size.height - 5 < text_lines.len() as u16 {
if optional_lines.len() == 0 {
break;
}
text_lines.remove(optional_lines[0] as usize);
optional_lines.remove(0);
}
needed_height += text_lines.len();
let q_informer = if self.update_needed {
"Q to Exit"
} else {
"Q to Cancel"
};
if size.width < 60 || size.height < needed_height as u16 {
let width_style = if size.width > 76 {
Style::default().fg(Color::Green)
} else if size.width >= 60 {
Style::default().fg(Color::Yellow)
} else {
Style::default().fg(Color::Red)
};
let height_style = if size.height > 20 {
Style::default().fg(Color::Green)
} else if size.height >= (header_lines as u16 + 10) {
Style::default().fg(Color::Yellow)
} else {
Style::default().fg(Color::Red)
};
let warning_text = Text::from(vec![
Line::from(vec![
Span::raw("Width: "),
Span::styled(format!("{}", size.width), width_style),
Span::raw(" / 60"),
]),
Line::from(vec![
Span::raw("Height: "),
Span::styled(format!("{}", size.height), height_style),
Span::raw(format!(" / {}", header_lines + 10)),
]),
]);
let warning = Paragraph::new(warning_text)
.alignment(Alignment::Center)
.block(
Block::default()
.borders(Borders::ALL)
.title(format!("UI Too Small [{}]", q_informer)),
);
f.render_widget(warning, size);
return;
}
let consent_block = Paragraph::new(Text::from(text_lines)).block(
Block::default()
.title(format!(" Update Tensamin User Consent [{}] ", q_informer))
.borders(Borders::ALL),
);
f.render_widget(consent_block, chunks[0]);
let downgrade_scenario = self.tos_needed || self.pp_needed;
draw_buttons(
f,
chunks[1],
self.focus,
(self.eula, self.tos && self.pp),
self.update_needed,
downgrade_scenario,
self.pp_needed || self.tos_needed,
);
}
fn handle_input(&mut self, event: KeyEvent) -> InteractionResult {
let mut possible_states = Vec::new();
if self.eula_needed {
possible_states.push(Focus::Eula);
}
if self.tos_needed {
possible_states.push(Focus::Tos);
}
if self.pp_needed {
possible_states.push(Focus::Pp);
}
possible_states.push(Focus::Cancel);
if self.eula {
possible_states.push(Focus::Continue);
if self.tos && self.pp {
possible_states.push(Focus::ContinueAll);
}
}
match event.code {
KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => {
if let Some(sender) = self.sender.take() {
let _ = sender.send(UserChoice::Deny);
}
return InteractionResult::CloseScreen;
}
KeyCode::Enter | KeyCode::Char(' ') => match self.focus {
Focus::Eula => {
self.eula = !self.eula;
self.tos = !self.tos_needed;
self.pp = !self.pp_needed;
InteractionResult::Handled
}
Focus::Tos if self.eula => {
self.tos = !self.tos;
InteractionResult::Handled
}
Focus::Pp if self.eula => {
self.pp = !self.pp;
InteractionResult::Handled
}
Focus::Cancel => {
if let Some(sender) = self.sender.take() {
let _ = sender.send(UserChoice::Deny);
}
InteractionResult::CloseScreen
}
Focus::Continue if self.eula => {
if let Some(sender) = self.sender.take() {
let _ = sender.send(UserChoice::AcceptEULA);
}
InteractionResult::CloseScreen
}
Focus::ContinueAll if self.eula && self.tos && self.pp => {
if let Some(sender) = self.sender.take() {
let _ = sender.send(UserChoice::AcceptAll);
}
InteractionResult::CloseScreen
}
_ => InteractionResult::Unhandled,
},
KeyCode::Char('o') | KeyCode::Char('O') => {
let terms_type = match self.focus {
Focus::Eula => Some(Type::EULA),
Focus::Tos => Some(Type::TOS),
Focus::Pp => Some(Type::PP),
_ => None,
};
if let Some(terms_type) = terms_type {
let fut = Box::pin(async move {
let content = get_terms(terms_type.clone()).await.unwrap();
Box::new(FileViewer::new(terms_type.to_string(), &content))
as Box<dyn Screen>
});
return InteractionResult::OpenFutureScreen { screen: fut };
} else {
InteractionResult::Unhandled
}
}
KeyCode::Char('l') | KeyCode::Char('L') => match self.focus {
Focus::Eula => {
let _ = open::that(get_newest_link(Type::EULA));
InteractionResult::Handled
}
Focus::Tos => {
let _ = open::that(get_newest_link(Type::TOS));
InteractionResult::Handled
}
Focus::Pp => {
let _ = open::that(get_newest_link(Type::PP));
InteractionResult::Handled
}
_ => InteractionResult::Unhandled,
},
KeyCode::Up | KeyCode::Left => {
self.focus.prev(&possible_states);
InteractionResult::Handled
}
KeyCode::Down | KeyCode::Right | KeyCode::Tab => {
self.focus.next(&possible_states);
InteractionResult::Handled
}
_ => InteractionResult::Unhandled,
}
}
}

View file

View file

@ -1,167 +0,0 @@
use crate::{
ACTIVE_TASKS, SHUTDOWN,
gui::{
input_handler::setup_input_handler, interaction_result::InteractionResult,
screens::screens::Screen,
},
};
use crossterm::event::KeyEvent;
use once_cell::sync::Lazy;
use ratatui::{Terminal, backend::CrosstermBackend, init};
use std::{
collections::VecDeque,
io::Stdout,
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
},
time::Duration,
};
use tokio::{sync::RwLock, time::Instant};
/// UI state and rendering
pub static UNIQUE: AtomicBool = AtomicBool::new(true);
pub static FPS: Lazy<RwLock<(f64, f64)>> = Lazy::new(|| RwLock::new((0.0, 0.0)));
pub struct UI {
pub terminal: Arc<Mutex<Terminal<CrosstermBackend<Stdout>>>>,
screen_stack: Arc<RwLock<Vec<Box<dyn Screen>>>>,
}
pub fn start_tui() -> Arc<UI> {
let ui = Arc::new(UI::new());
let uic = ui.clone();
ACTIVE_TASKS.insert("UI Renderer".to_string());
tokio::spawn(async move {
let mut last_render = Instant::now();
let mut fps_samples: VecDeque<f64> = VecDeque::with_capacity(20);
let mut skip_samples: VecDeque<u16> = VecDeque::with_capacity(20);
let mut fps_sum = 0.0;
let mut skip_sum: u32 = 0;
let mut skipped = 0;
loop {
if *SHUTDOWN.read().await {
break;
}
if skipped > 5 || UNIQUE.load(Ordering::Relaxed) {
uic.render().await;
skip_samples.push_back(skipped);
skip_sum += skipped as u32;
if skip_samples.len() > 20 {
if let Some(old) = skip_samples.pop_front() {
skip_sum -= old as u32;
}
}
skipped = 0;
let elapsed = last_render.elapsed().as_secs_f64();
if elapsed > 0.0 {
let fps = 1.0 / elapsed;
fps_samples.push_back(fps);
fps_sum += fps;
if fps_samples.len() > 20 {
if let Some(old) = fps_samples.pop_front() {
fps_sum -= old;
}
}
}
let avg_fps = if !fps_samples.is_empty() {
fps_sum / fps_samples.len() as f64
} else {
0.0
};
let avg_skips_percentage = if !skip_samples.is_empty() {
let avg_skipped = skip_sum as f64 / skip_samples.len() as f64;
let total_iterations = avg_skipped + 1.0;
(avg_skipped / total_iterations) * 100.0
} else {
0.0
};
*FPS.write().await = (avg_fps, avg_skips_percentage);
last_render = Instant::now();
UNIQUE.store(false, Ordering::Relaxed);
} else {
skipped += 1;
}
tokio::time::sleep(Duration::from_millis(16)).await;
}
ACTIVE_TASKS.remove("UI Renderer");
ratatui::restore();
});
setup_input_handler(ui.clone());
ui
}
impl UI {
pub fn new() -> Self {
let terminal = init();
Self {
terminal: Arc::new(Mutex::new(terminal)),
screen_stack: Arc::new(RwLock::new(Vec::new())),
}
}
pub async fn set_screen(&self, screen: Box<dyn Screen>) {
self.screen_stack.write().await.push(screen);
}
pub async fn replace_screen(&self, screen: Box<dyn Screen>) {
let mut stack = self.screen_stack.write().await;
stack.pop();
stack.push(screen);
}
pub async fn handle_input(self: Arc<Self>, key_event: KeyEvent) {
let result = {
let mut stack = self.screen_stack.write().await;
if let Some(screen) = stack.last_mut() {
screen.handle_input(key_event)
} else {
return;
}
};
match result {
InteractionResult::OpenScreen { screen } => {
self.set_screen(screen).await;
}
InteractionResult::OpenFutureScreen { screen: fut } => {
let ui = self.clone();
let screen = fut.await;
ui.set_screen(screen).await;
}
InteractionResult::CloseScreen => {
let mut stack = self.screen_stack.write().await;
stack.pop();
if stack.is_empty() {
*SHUTDOWN.write().await = true;
}
}
InteractionResult::Handled => {}
InteractionResult::Unhandled => {}
}
}
pub async fn render(&self) {
if let Some(screen) = self.screen_stack.read().await.last() {
let mut terminal = self.terminal.lock().unwrap();
terminal
.draw(|f| {
screen.render(f, f.area());
})
.unwrap();
}
}
}

View file

@ -1,63 +0,0 @@
use ratatui::layout::Rect;
use ratatui::prelude::*;
use ratatui::style::Style;
use ratatui::widgets::Borders;
fn set_join_char(frame: &mut Frame, x: u16, y: u16, c: char) {
frame
.buffer_mut()
.set_string(x, y, c.to_string(), Style::default());
}
pub fn draw_block_joins(frame: &mut Frame, area: Rect, borders: Borders, joins: Borders) {
let x0 = area.x;
let y0 = area.y;
let x1 = area.x + area.width - 1;
let y1 = area.y + area.height - 1;
if borders.contains(Borders::TOP) && borders.contains(Borders::LEFT) {
let top_left = match (joins.contains(Borders::TOP), joins.contains(Borders::LEFT)) {
(true, true) => '┼',
(true, false) => '├',
(false, true) => '┬',
(false, false) => '┌',
};
set_join_char(frame, x0, y0, top_left);
}
if borders.contains(Borders::TOP) && borders.contains(Borders::RIGHT) {
let top_right = match (joins.contains(Borders::TOP), joins.contains(Borders::RIGHT)) {
(true, true) => '┼',
(true, false) => '┤',
(false, true) => '┬',
(false, false) => '┐',
};
set_join_char(frame, x1, y0, top_right);
}
if borders.contains(Borders::BOTTOM) && borders.contains(Borders::LEFT) {
let bottom_left = match (
joins.contains(Borders::BOTTOM),
joins.contains(Borders::LEFT),
) {
(true, true) => '┼',
(true, false) => '├',
(false, true) => '┴',
(false, false) => '└',
};
set_join_char(frame, x0, y1, bottom_left);
}
if borders.contains(Borders::BOTTOM) && borders.contains(Borders::RIGHT) {
let bottom_right = match (
joins.contains(Borders::BOTTOM),
joins.contains(Borders::RIGHT),
) {
(true, true) => '┼',
(true, false) => '┤',
(false, true) => '┴',
(false, false) => '┘',
};
set_join_char(frame, x1, y1, bottom_right);
}
}

View file

@ -1,80 +0,0 @@
use crate::util::file_util::save_file;
use json::{self, JsonError, JsonValue};
pub fn create_languages() -> Result<(), JsonError> {
let mut frontend_messages = JsonValue::new_object();
let mut omikron_messages = JsonValue::new_object();
let mut button_texts = JsonValue::new_object();
let mut general_texts = JsonValue::new_object();
let mut debug_messages = JsonValue::new_object();
frontend_messages.insert("error", "An error occurred")?;
// FRONTEND
frontend_messages.insert("get_chats", "User {} is loading conversations")?;
frontend_messages.insert("message_get", "User {} is loading messages")?;
frontend_messages.insert("get_communities", "User {} is loading communities")?;
frontend_messages.insert("client_connected", "Client {} connected")?;
frontend_messages.insert("add_conversation", "User {} added {}")?;
frontend_messages.insert("message_send", "User {} sent a message")?;
// OMIKRON
omikron_messages.insert(
"identification_response",
"IOTA identified on Omikron, {} users!",
)?;
omikron_messages.insert(
"send_message_failed",
"Failed to send message to Omikron: {}",
)?;
omikron_messages.insert("connection_failed", "Failed to connect to Omikron: {}")?;
// BUTTONS
button_texts.insert("exit", "Exit")?;
// GENERAL
general_texts.insert("iota_id", "IOTA ID: {}-####-####-####-############")?;
general_texts.insert("user_id", "USER ID: {}")?;
general_texts.insert("user_ids", "USER IDS: {}")?;
general_texts.insert("user_load_failed", "Failed to load user data")?;
general_texts.insert("setup_completed", "Launched")?;
general_texts.insert(
"community_active",
"Communities active on ws://{}:{}/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(
"languages/en_INT",
"frontend.json",
&frontend_messages.to_string(),
);
save_file(
"languages/en_INT",
"omikron.json",
&omikron_messages.to_string(),
);
save_file(
"languages/en_INT",
"buttons.json",
&button_texts.to_string(),
);
save_file(
"languages/en_INT",
"debug.json",
&debug_messages.to_string(),
);
save_file(
"languages/en_INT",
"general.json",
&general_texts.to_string(),
);
Ok(())
}

View file

@ -1,105 +0,0 @@
use crate::util::file_util::{self};
use json::parse;
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::sync::Mutex;
#[derive(Clone)]
pub struct LanguagePack {
language: HashMap<String, String>,
}
pub static LANGUAGE_PACK: Lazy<Mutex<LanguagePack>> =
Lazy::new(|| Mutex::new(LanguagePack::new("en_INT")));
#[allow(dead_code)]
pub fn get_language() -> LanguagePack {
LANGUAGE_PACK.lock().unwrap().clone()
}
#[allow(dead_code)]
pub fn get_languages() -> Vec<String> {
file_util::get_children("languages")
}
#[allow(dead_code)]
pub fn set_language(language: &str) {
LANGUAGE_PACK.lock().unwrap().language.clear();
LANGUAGE_PACK.lock().unwrap().load_language(language);
}
pub fn from_key(key: &str) -> String {
LANGUAGE_PACK
.lock()
.unwrap()
.get_translation(key)
.to_string()
}
pub fn format(key: &str, args: &[&str]) -> String {
let message = from_key(key);
let mut formatted = String::new();
let parts = message.split("{}");
for (i, part) in parts.enumerate() {
formatted.push_str(part);
if i < args.len() {
formatted.push_str(args[i]);
}
}
formatted
}
impl LanguagePack {
pub fn new(language: &str) -> Self {
let mut pack = LanguagePack {
language: HashMap::new(),
};
pack.load_language(language);
pack
}
pub fn load_language(&mut self, language: &str) {
let path = format!("languages/{}/", language);
let frontend_messages = file_util::load_file(&path, "frontend.json");
let frontend_messages = parse(&frontend_messages).unwrap();
for (key, value) in frontend_messages.entries() {
self.language
.insert(key.to_string(), value.as_str().unwrap().to_string());
}
let omikron_messages = file_util::load_file(&path, "omikron.json");
let omikron_messages = parse(&omikron_messages).unwrap();
for (key, value) in omikron_messages.entries() {
self.language
.insert(key.to_string(), value.as_str().unwrap().to_string());
}
let button_texts = file_util::load_file(&path, "buttons.json");
let button_texts = parse(&button_texts).unwrap();
for (key, value) in button_texts.entries() {
self.language
.insert(key.to_string(), value.as_str().unwrap().to_string());
}
let debug_messages = file_util::load_file(&path, "debug.json");
let debug_messages = parse(&debug_messages).unwrap();
for (key, value) in debug_messages.entries() {
self.language
.insert(key.to_string(), value.as_str().unwrap().to_string());
}
let general_messages = file_util::load_file(&path, "general.json");
let general_messages = parse(&general_messages).unwrap();
for (key, value) in general_messages.entries() {
self.language
.insert(key.to_string(), value.as_str().unwrap().to_string());
}
}
pub fn get_translation(&self, key: &str) -> String {
match self.language.get(key) {
Some(v) if !v.is_empty() => v.clone(),
_ => key.to_uppercase(),
}
}
}

View file

@ -1,2 +0,0 @@
pub mod language_creator;
pub mod language_manager;

View file

@ -1,183 +1,51 @@
use dashmap::DashSet;
use once_cell::sync::Lazy;
use pnet::datalink::NetworkInterface;
use std::sync::Arc;
use std::sync::LazyLock;
use std::sync::Mutex;
use tokio::sync::RwLock;
use tokio::time::{Duration, sleep};
mod auth;
mod gui;
mod langu;
mod omikron;
mod terms;
mod users;
mod anonymous_clients;
mod calls;
mod data;
mod omega;
mod rho;
mod util;
use crate::gui::app_state;
use crate::gui::app_state::AppState;
use crate::gui::screens::main_screen::MainScreen;
use crate::gui::ui::start_tui;
use crate::langu::language_creator;
use crate::omikron::omikron_connection::OmikronConnection;
use crate::terms::consent_state;
use crate::users::user_manager;
use crate::util::config_util::CONFIG;
use crate::util::file_util::download_and_extract_zip;
use crate::util::file_util::has_dir;
use crate::util::logger;
use std::env;
pub static APP_STATE: LazyLock<Arc<Mutex<AppState>>> =
LazyLock::new(|| Arc::new(Mutex::new(AppState::new())));
use dotenv::dotenv;
use once_cell::sync::Lazy;
use rustls::crypto::aws_lc_rs::default_provider;
pub static SHUTDOWN: Lazy<RwLock<bool>> = Lazy::new(|| RwLock::new(false));
pub static RELOAD: Lazy<RwLock<bool>> = Lazy::new(|| RwLock::new(true));
pub static ACTIVE_TASKS: Lazy<DashSet<String>> = Lazy::new(|| DashSet::new());
use crate::{
calls::call_util::garbage_collect_calls,
omega::omega_connection::get_omega_connection,
rho::server::start,
util::{
crypto_helper::{load_public_key, load_secret_key},
logger::startup,
},
};
#[tokio::main(flavor = "multi_thread", worker_threads = 16)]
#[allow(unused_must_use, dead_code)]
static PRIVATE_KEY: Lazy<String> = Lazy::new(|| env::var("PRIVATE_KEY").unwrap());
pub fn get_private_key() -> x448::Secret {
load_secret_key(&*PRIVATE_KEY).unwrap()
}
static PUBLIC_KEY: Lazy<String> = Lazy::new(|| env::var("PUBLIC_KEY").unwrap());
pub fn get_public_key() -> x448::PublicKey {
load_public_key(&*PUBLIC_KEY).unwrap()
}
#[tokio::main]
async fn main() {
while *RELOAD.read().await {
*RELOAD.write().await = false;
*SHUTDOWN.write().await = false;
let ui = start_tui();
let (eula, tos_pp) = consent_state::check(ui.clone()).await;
if !eula {
*SHUTDOWN.write().await = true;
loop {
if ACTIVE_TASKS.is_empty() {
break;
}
sleep(Duration::from_millis(100)).await;
}
println!("You need to accept our End User Licence Agreement before launching!");
println!("You can find this at 'agreements'!");
return;
}
if !tos_pp {
*SHUTDOWN.write().await = true;
loop {
if ACTIVE_TASKS.is_empty() {
break;
}
sleep(Duration::from_millis(100)).await;
}
println!(
"Please accept our Privacy Policy & Terms of Serivce before using Tensamin Services!"
);
println!("In future releases this will be optional!");
println!("You can find this at 'agreements'!");
return;
}
app_state::setup();
let main_screen = MainScreen::new(ui.clone()).await;
ui.set_screen(Box::new(main_screen)).await;
// LANGUAGE PACK
if let Err(e) = language_creator::create_languages() {
println!("Language pack creation failed: {}", e);
return;
}
// UI
logger::startup();
// BASIC CONFIGURATION
&CONFIG.write().await.load();
// USER MANAGEMENT
if let Err(_) = user_manager::load_users().await {
log_t!("user_load_failed");
}
if let Err(_) = default_provider().install_default() {
println!("Error loading Provider");
return;
}
dotenv().ok();
startup();
let mut sb = "".to_string();
for up in user_manager::get_users() {
sb = sb + "," + &up.user_id.to_string().as_str();
}
if !sb.is_empty() {
{}
sb.remove(0);
sb = sb + ",";
get_omega_connection();
tokio::spawn(async move {
if let Err(e) = start(959).await {
log_err!(0, util::logger::PrintType::General, "{}", e);
}
log!(
"IOTA ID: {}",
CONFIG.read().await.get_iota_id().to_string()
);
log!("User IDS: {}", sb);
});
garbage_collect_calls();
// 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!("Community IDS: {}", sb1); */
let port = CONFIG.read().await.get_port();
let mut ip = "0.0.0.0".to_string();
for iface in pnet::datalink::interfaces() {
let iface: NetworkInterface = iface;
if iface.ips.len() > 0 {
let ipsv = format!("{}", iface.ips[0]);
let ips: &str = ipsv.split('/').next().unwrap_or("");
if format!("{}", ips).starts_with("10.") || format!("{}", ips).starts_with("192.") {
ip = ips.to_string();
}
}
}
/*
if start(port).await {
log_t!("community_active", ip, port.to_string());
} else {
if port < 1024 {
log_t!("community_start_error_admin", port.to_string());
} else {
log_t!("community_start_error", port.to_string());
}
} */
if !has_dir("web") {
download_and_extract_zip(
"https://omega.tensamin.net/api/download/iota_frontend",
"web",
)
.await;
}
let _ = omikron::omikron_connection::get_omikron_connection().await;
log_t!("setup_completed");
loop {
if *SHUTDOWN.read().await {
break;
}
sleep(Duration::from_millis(100)).await;
}
if *RELOAD.read().await {
loop {
if ACTIVE_TASKS.is_empty() {
break;
}
sleep(Duration::from_secs(1)).await;
}
&CONFIG.write().await.clear();
user_manager::clear();
/*community_manager::clear();*/
*APP_STATE.lock().unwrap() = AppState::new();
}
ui.terminal.lock().unwrap().clear();
ui.terminal.lock().unwrap().flush();
}
tokio::signal::ctrl_c().await.unwrap();
}

1
src/omega/mod.rs Normal file
View file

@ -0,0 +1 @@
pub mod omega_connection;

View file

@ -0,0 +1,741 @@
use crate::{
data::user::UserStatus,
get_private_key, log, log_cv_in, log_cv_out, log_err, log_in,
rho::rho_manager::{self, RHO_CONNECTIONS, connection_count},
util::{
crypto_helper::{decrypt_b64, secret_key_to_base64},
file_util::load_file_vec,
logger::PrintType,
},
};
use dashmap::DashMap;
use once_cell::sync::Lazy;
use std::{collections::HashMap, env, sync::Arc, time::Duration};
use tokio::{
sync::{Mutex, RwLock, mpsc, watch},
task::JoinHandle,
time::{Instant, sleep},
};
use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue, rand_u32};
use ttp_native::{Receiver, Sender};
use uuid::Uuid;
// ============================================================================
// Configuration
// ============================================================================
const OMEGA_HOST_DEFAULT: &str = "methanium.net";
const OMEGA_PORT_DEFAULT: u16 = 9187;
const RECONNECT_DELAY: Duration = Duration::from_secs(5);
const MAX_RECONNECT_DELAY: Duration = Duration::from_secs(300);
const CONNECTION_TIMEOUT: Duration = Duration::from_secs(10);
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
const TASK_CLEANUP_INTERVAL: Duration = Duration::from_secs(60);
const TASK_MAX_AGE: Duration = Duration::from_secs(60);
// ============================================================================
// Waiting Task System
// ============================================================================
pub struct WaitingTask {
pub task: Box<dyn Fn(Arc<OmegaConnection>, CommunicationValue) -> bool + Send + Sync>,
pub inserted_at: Instant,
}
pub static WAITING_TASKS: Lazy<DashMap<u32, WaitingTask>> = Lazy::new(DashMap::new);
pub fn start_task_cleanup_loop() {
tokio::spawn(async {
loop {
sleep(TASK_CLEANUP_INTERVAL).await;
WAITING_TASKS.retain(|_, v| v.inserted_at.elapsed() < TASK_MAX_AGE);
}
});
}
// ============================================================================
// Connection State
// ============================================================================
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ConnectionState {
Disconnected,
Connecting,
Connected { identified: bool },
}
#[allow(unused_variables)]
impl ConnectionState {
pub fn is_connected(&self) -> bool {
match self {
ConnectionState::Connected { identified } => true,
_ => false,
}
}
#[allow(dead_code)]
pub fn is_identified(&self) -> bool {
match self {
ConnectionState::Connected { identified: true } => true,
_ => false,
}
}
}
// ============================================================================
// Omega Connection (Client-side with auto-reconnect)
// ============================================================================
#[allow(dead_code)]
pub struct OmegaConnection {
state: Arc<RwLock<ConnectionState>>,
sender: Arc<RwLock<Option<Arc<Sender>>>>,
connection_loop_handle: Arc<Mutex<Option<JoinHandle<()>>>>,
host: String,
port: u16,
server_cert: Vec<u8>,
last_ping: Arc<Mutex<i64>>,
heartbeat_handle: Arc<Mutex<Option<JoinHandle<()>>>>,
message_send_times: Arc<Mutex<HashMap<Uuid, Instant>>>,
pub connection_id: Uuid,
shutdown_tx: Arc<Mutex<Option<watch::Sender<bool>>>>,
// Track if we should reconnect on close
reconnect_on_close: Arc<RwLock<bool>>,
}
impl OmegaConnection {
pub fn new() -> Self {
Self::with_host(OMEGA_HOST_DEFAULT, OMEGA_PORT_DEFAULT)
}
pub fn with_host(host: &str, port: u16) -> Self {
// Load server certificate from default location
let server_cert =
load_file_vec("certs", "cert.pem").expect("Failed to load server certificate");
Self::with_host_and_cert(host, port, server_cert)
}
// New constructor that accepts certificate directly
pub fn with_host_and_cert(host: &str, port: u16, server_cert: Vec<u8>) -> Self {
let (shutdown_tx, _) = watch::channel(false);
OmegaConnection {
state: Arc::new(RwLock::new(ConnectionState::Disconnected)),
sender: Arc::new(RwLock::new(None)),
connection_loop_handle: Arc::new(Mutex::new(None)),
host: host.to_string(),
port,
server_cert,
last_ping: Arc::new(Mutex::new(-1)),
heartbeat_handle: Arc::new(Mutex::new(None)),
message_send_times: Arc::new(Mutex::new(HashMap::new())),
connection_id: Uuid::new_v4(),
shutdown_tx: Arc::new(Mutex::new(Some(shutdown_tx))),
reconnect_on_close: Arc::new(RwLock::new(true)),
}
}
// -------------------------------------------------------------------------
// Connection Management
// -------------------------------------------------------------------------
pub async fn start(self: Arc<Self>) {
// Cancel any existing connection loop
if let Some(handle) = self.connection_loop_handle.lock().await.take() {
handle.abort();
}
// Set reconnect flag
*self.reconnect_on_close.write().await = true;
let self_clone = self.clone();
let handle = tokio::spawn(async move {
self_clone.connection_loop().await;
});
*self.connection_loop_handle.lock().await = Some(handle);
}
#[allow(dead_code)]
pub async fn stop(&self) {
// Disable reconnection
*self.reconnect_on_close.write().await = false;
if let Some(tx) = self.shutdown_tx.lock().await.take() {
let _ = tx.send(true);
}
if let Some(handle) = self.connection_loop_handle.lock().await.take() {
handle.abort();
}
if let Some(handle) = self.heartbeat_handle.lock().await.take() {
handle.abort();
}
// Close sender if connected
if let Some(sender) = self.sender.read().await.as_ref() {
sender.close();
}
*self.state.write().await = ConnectionState::Disconnected;
*self.sender.write().await = None;
}
async fn connection_loop(self: Arc<Self>) {
let mut reconnect_delay = RECONNECT_DELAY;
let shutdown_rx = self.shutdown_tx.lock().await.as_ref().unwrap().subscribe();
let mut shutdown_rx = shutdown_rx;
loop {
if *shutdown_rx.borrow() {
log_in!(0, PrintType::Omega, "Connection loop shutting down");
break;
}
// Check if reconnection is enabled
if !*self.reconnect_on_close.read().await {
log_in!(0, PrintType::Omega, "Reconnection disabled, exiting loop");
break;
}
match self.clone().connect_once().await {
Ok(()) => {
// Connection closed gracefully, check if we should reconnect
if *self.reconnect_on_close.read().await {
log_err!(
0,
PrintType::Omega,
"Connection lost, reconnecting in {:?}...",
reconnect_delay
);
} else {
log_in!(0, PrintType::Omega, "Connection closed, not reconnecting");
break;
}
}
Err(e) => {
log_err!(
0,
PrintType::Omega,
"Connection failed: {}, retrying in {:?}...",
e,
reconnect_delay
);
}
}
tokio::select! {
_ = sleep(reconnect_delay) => {}
_ = shutdown_rx.changed() => {
if *shutdown_rx.borrow() {
break;
}
}
}
reconnect_delay = std::cmp::min(reconnect_delay * 2, MAX_RECONNECT_DELAY);
}
}
async fn connect_once(self: Arc<Self>) -> Result<(), String> {
*self.state.write().await = ConnectionState::Connecting;
let addr_str = format!("https://{}:{}", self.host, self.port);
let (sender, mut receiver) = ttp_native::client::connect(&addr_str, None)
.await
.map_err(|e| format!("Connection failed: {}", e))?;
log_in!(
0,
PrintType::Omega,
"QUIC connection established to {}",
addr_str
);
// Store sender
let sender_arc = Arc::new(sender);
*self.sender.write().await = Some(sender_arc.clone());
*self.state.write().await = ConnectionState::Connected { identified: false };
// Get handle for close monitoring
let sender_handle = sender_arc.handle().clone();
// Start read loop
let read_self = self.clone();
let read_handle = tokio::spawn(async move {
read_self.read_loop(&mut receiver, sender_handle).await;
});
// Send identification
self.send_identification().await;
// Start heartbeat
let heartbeat_self = self.clone();
let heartbeat_handle = tokio::spawn(async move {
heartbeat_self.heartbeat_loop().await;
});
*self.heartbeat_handle.lock().await = Some(heartbeat_handle);
// Wait for read loop to complete (connection closed)
let result = read_handle.await;
// Cleanup
*self.sender.write().await = None;
*self.state.write().await = ConnectionState::Disconnected;
if let Some(handle) = self.heartbeat_handle.lock().await.take() {
handle.abort();
}
match result {
Ok(()) => {
// Check if we should reconnect
if *self.reconnect_on_close.read().await {
Err("Connection closed, will reconnect".to_string())
} else {
Ok(())
}
}
Err(e) => Err(format!("Read loop error: {}", e)),
}
}
// -------------------------------------------------------------------------
// Identification Handshake
// -------------------------------------------------------------------------
async fn send_identification(&self) {
let id = rand_u32();
let omikron_id = env::var("ID")
.unwrap_or("0".to_string())
.parse::<i64>()
.unwrap_or(0);
let identify_msg = CommunicationValue::new(CommunicationType::identification)
.with_id(id)
.add_data(DataTypes::omikron_id, DataValue::Number(omikron_id));
WAITING_TASKS.insert(
id,
WaitingTask {
task: Box::new(|selfc, cv| {
if cv.is_type(CommunicationType::error_not_found) {
log_err!(
0,
PrintType::Omega,
"Identification failed: Omikron ID not found"
);
return false;
}
if !cv.is_type(CommunicationType::challenge) {
return false;
}
tokio::spawn(async move {
if let Err(e) = selfc.handle_challenge(cv).await {
log_err!(0, PrintType::Omega, "Challenge handling failed: {}", e);
}
});
true
}),
inserted_at: Instant::now(),
},
);
self.send_message(&identify_msg).await;
}
async fn handle_challenge(&self, cv: CommunicationValue) -> Result<(), String> {
let challenge = cv
.get_data(DataTypes::challenge)
.as_str()
.ok_or("Challenge not found")?;
let server_pub_key = cv
.get_data(DataTypes::public_key)
.as_str()
.ok_or("Public key not found")?;
let decrypted_challenge = decrypt_b64(
&secret_key_to_base64(&get_private_key()),
server_pub_key,
challenge,
)
.map_err(|e| format!("Decryption failed: {:?}", e))?;
let response_msg = CommunicationValue::new(CommunicationType::challenge_response)
.with_id(cv.get_id())
.add_data(DataTypes::challenge, DataValue::Str(decrypted_challenge));
let response_id = response_msg.get_id();
WAITING_TASKS.insert(
response_id,
WaitingTask {
task: Box::new(|selfc, final_cv| {
if !final_cv.is_type(CommunicationType::identification_response) {
log_err!(0, PrintType::Omega, "Expected identification_response");
return false;
}
let accepted = final_cv
.get_data(DataTypes::accepted)
.as_bool()
.unwrap_or(false);
if !accepted {
log_err!(0, PrintType::Omega, "Omega did not accept identification");
return false;
}
tokio::spawn(async move {
let mut state = selfc.state.write().await;
if let ConnectionState::Connected { identified: _ } = *state {
*state = ConnectionState::Connected { identified: true };
}
drop(state);
selfc.sync_client_iota_status().await;
});
log!(0, PrintType::Omega, "Successfully identified with Omega");
true
}),
inserted_at: Instant::now(),
},
);
self.send_message(&response_msg).await;
Ok(())
}
async fn sync_client_iota_status(self: Arc<Self>) {
let mut connected_iota_ids: Vec<DataValue> = Vec::new();
let mut connected_user_ids: Vec<DataValue> = Vec::new();
let rho_connections_reader = RHO_CONNECTIONS.read().await;
for iota_id in rho_connections_reader.keys() {
connected_iota_ids.push(DataValue::Number(*iota_id));
}
for rho in rho_connections_reader.values() {
for client_conn in rho.get_client_connections().await {
connected_user_ids.push(DataValue::Number(client_conn.get_user_id().await as i64));
}
}
drop(rho_connections_reader);
let sync_msg = CommunicationValue::new(CommunicationType::sync_client_iota_status)
.add_data(DataTypes::iota_ids, DataValue::Array(connected_iota_ids))
.add_data(DataTypes::user_ids, DataValue::Array(connected_user_ids))
.add_data(
DataTypes::rho_connections,
DataValue::Number(connection_count().await as i64),
);
self.send_message(&sync_msg).await;
}
// -------------------------------------------------------------------------
// Read Loop & Heartbeat
// -------------------------------------------------------------------------
async fn read_loop(
self: Arc<Self>,
receiver: &mut Receiver,
sender_handle: Arc<ttp_native::ConnectionHandle>,
) {
// Monitor both receiver and sender handle for close
let mut close_rx = sender_handle.subscribe_close();
loop {
tokio::select! {
result = receiver.receive() => {
match result {
Ok(cv) => {
if !cv.is_type(CommunicationType::pong) && !cv.is_type(CommunicationType::ping) {
log_cv_in!(PrintType::Omega, &cv);
}
if cv.is_type(CommunicationType::pong) || cv.is_type(CommunicationType::ping) {
self.handle_pong(&cv).await;
continue;
}
let msg_id = cv.get_id();
if let Some((_, task)) = WAITING_TASKS.remove(&msg_id) {
if (task.task)(self.clone(), cv.clone()) {
continue;
}
}
if cv.is_type(CommunicationType::iota_user_data) {
if let DataValue::Array(users) = cv.get_data(DataTypes::user_ids) {
let mut user_ids: Vec<u64> = Vec::new();
for value in users {
if let DataValue::Number(user_id) = value {
user_ids.push(*user_id as u64);
}
}
let connections = crate::rho::rho_manager::RHO_CONNECTIONS.read().await;
if let Some(iota_id) = cv.get_data(DataTypes::iota_id).as_number() {
if let Some(rho) = connections.get(&iota_id) {
rho.get_iota_connection().set_user_ids(user_ids).await;
}
} else {
for rho in connections.values() {
rho.get_iota_connection().set_user_ids(user_ids.clone()).await;
}
}
}
}
}
Err(e) => {
log_err!(0, PrintType::Omega, "Receive error: {}", e);
break;
}
}
}
_ = close_rx.changed() => {
// Connection was closed by either side
if let Some(reason) = close_rx.borrow().clone() {
log_err!(0, PrintType::Omega, "Connection closed: {:?}", reason);
} else {
log_in!(0, PrintType::Omega, "Connection closed cleanly");
}
break;
}
}
}
}
async fn heartbeat_loop(self: Arc<Self>) {
loop {
sleep(HEARTBEAT_INTERVAL).await;
// Check if still connected
if !self.state.read().await.is_connected() {
break;
}
// Check if sender is closed
if let Some(sender) = self.sender.read().await.as_ref() {
if sender.is_closed() {
log_err!(0, PrintType::Omega, "Sender closed, stopping heartbeat");
break;
}
} else {
break;
}
self.send_ping().await;
}
}
async fn send_ping(&self) {
let ping = CommunicationValue::new(CommunicationType::ping).add_data(
DataTypes::send_time,
DataValue::Number(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64,
),
);
self.send_message(&ping).await;
}
async fn handle_pong(&self, cv: &CommunicationValue) {
let timestamp = cv
.get_data(DataTypes::send_time)
.as_number()
.unwrap_or_else(|| {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64
});
*self.last_ping.lock().await = timestamp;
}
// -------------------------------------------------------------------------
// Public API
// -------------------------------------------------------------------------
pub async fn send_message(&self, cv: &CommunicationValue) {
if !cv.is_type(CommunicationType::pong) && !cv.is_type(CommunicationType::ping) {
log_cv_out!(PrintType::Omega, &cv);
}
let sender_guard = self.sender.read().await;
if let Some(sender) = sender_guard.as_ref() {
// Check if closed before sending
if sender.is_closed() {
log_err!(0, PrintType::Omega, "Cannot send: connection closed");
drop(sender_guard);
// Trigger reconnection by closing the connection state
if let Some(sender) = self.sender.write().await.take() {
sender.close();
}
return;
}
let sender_clone = Arc::clone(sender);
drop(sender_guard);
if let Err(e) = sender_clone.send(cv).await {
log_err!(0, PrintType::Omega, "Send failed: {}", e);
}
} else {
log_err!(0, PrintType::Omega, "Cannot send: not connected");
}
}
pub async fn await_connection(&self, timeout_duration: Option<Duration>) -> Result<(), String> {
if self.state.read().await.is_connected() {
return Ok(());
}
let timeout = timeout_duration.unwrap_or(CONNECTION_TIMEOUT);
let start = Instant::now();
loop {
if self.state.read().await.is_connected() {
return Ok(());
}
if start.elapsed() >= timeout {
return Err(format!(
"Connection not established within {} seconds",
timeout.as_secs()
));
}
sleep(Duration::from_millis(100)).await;
}
}
pub async fn await_response(
&self,
cv: &CommunicationValue,
timeout_duration: Option<Duration>,
) -> Result<CommunicationValue, String> {
self.await_connection(timeout_duration).await?;
let (tx, mut rx) = mpsc::channel(1);
let msg_id = cv.get_id();
WAITING_TASKS.insert(
msg_id,
WaitingTask {
task: Box::new(move |_, response_cv| {
let inner_tx = tx.clone();
tokio::spawn(async move {
let _ = inner_tx.send(response_cv).await;
});
true
}),
inserted_at: Instant::now(),
},
);
self.send_message(cv).await;
let timeout = timeout_duration.unwrap_or(Duration::from_secs(10));
match tokio::time::timeout(timeout, rx.recv()).await {
Ok(Some(response_cv)) => Ok(response_cv),
Ok(_) => Err("Channel closed".to_string()),
Err(_) => {
WAITING_TASKS.remove(&msg_id);
Err("Request timed out".to_string())
}
}
}
#[allow(dead_code)]
pub async fn is_connected(&self) -> bool {
self.state.read().await.is_connected()
}
#[allow(dead_code)]
pub async fn is_identified(&self) -> bool {
self.state.read().await.is_identified()
}
#[allow(dead_code)]
pub async fn close_iota(iota_id: i64) {
let cv = CommunicationValue::new(CommunicationType::iota_disconnected)
.add_data(DataTypes::iota_id, DataValue::Number(iota_id));
OMEGA_CONNECTION.send_message(&cv).await;
}
pub async fn client_changed(_iota_id: i64, user_id: i64, state: UserStatus) {
let msg_type = match state {
UserStatus::iota_offline => CommunicationType::user_disconnected,
UserStatus::user_offline => CommunicationType::user_disconnected,
_ => CommunicationType::user_connected,
};
let cv = CommunicationValue::new(msg_type)
.add_data(DataTypes::user_id, DataValue::Number(user_id));
OMEGA_CONNECTION.send_message(&cv).await;
}
pub async fn user_states(user_id: i64, user_ids: Vec<i64>) {
let user_ids = user_ids.iter().map(|v| DataValue::Number(*v)).collect();
let cv = CommunicationValue::new(CommunicationType::get_states)
.add_data(DataTypes::user_ids, DataValue::Array(user_ids));
let msg_id = cv.get_id();
WAITING_TASKS.insert(
msg_id,
WaitingTask {
task: Box::new(
move |_: Arc<OmegaConnection>, response: CommunicationValue| {
tokio::spawn(async move {
let rho = rho_manager::get_rho_con_for_user(user_id).await;
if let Some(rho) = rho {
for client in rho.get_client_connections_for_user(user_id).await {
client.send_message(&response).await;
}
}
});
true
},
),
inserted_at: Instant::now(),
},
);
OMEGA_CONNECTION.send_message(&cv).await;
}
}
// ============================================================================
// Global Instance
// ============================================================================
static OMEGA_CONNECTION: Lazy<Arc<OmegaConnection>> = Lazy::new(|| {
let conn = Arc::new(OmegaConnection::new());
// Start the connection manager immediately
let conn_clone = conn.clone();
tokio::spawn(async move {
conn_clone.start().await;
});
start_task_cleanup_loop();
conn
});
pub fn get_omega_connection() -> Arc<OmegaConnection> {
OMEGA_CONNECTION.clone()
}

View file

@ -0,0 +1,41 @@
use std::time::Duration;
use tokio::time::Instant;
use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue, rand_u32};
use crate::omega::omega_connection::OmegaConnection;
const PING_TIMEOUT: Duration = Duration::from_secs(30);
impl OmegaConnection {
pub async fn send_ping(&self) {
let id = rand_u32();
let send_time = Instant::now();
let mut message_send_times = self.message_send_times.lock().await;
message_send_times.retain(|_uuid, time| time.elapsed() < PING_TIMEOUT);
message_send_times.insert(id as i64, send_time);
self.send_ping_message(id).await;
}
pub async fn send_ping_message(&self, id: u32) {
let ping_message = CommunicationValue::new(CommunicationType::ping)
.with_id(id)
.add_data(
DataTypes::last_ping,
DataValue::Number(self.last_ping.lock().await.unwrap()),
);
self.send_message(&ping_message).await;
}
/// Handles incoming pong and calculates latency
pub async fn handle_pong(&self, cv: &CommunicationValue, _log: bool) {
let id = cv.get_id();
let mut message_send_times = self.message_send_times.lock().await;
if let Some(send_time) = message_send_times.remove(&(id as i64)) {
let ping = Instant::now().duration_since(send_time).as_millis() as i64;
*self.last_ping.lock().await = ping;
}
}
}

View file

@ -1,2 +0,0 @@
pub mod omikron_connection;
pub mod ping_pong_task;

File diff suppressed because it is too large Load diff

View file

@ -1,38 +0,0 @@
use crate::omikron::omikron_connection::OmikronConnection;
use crate::{APP_STATE, log};
use dashmap::DashMap;
use std::sync::LazyLock;
use std::time::Instant;
use tokio::time::Duration;
use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue, rand_u32};
static PING_TIMES: LazyLock<DashMap<u32, Instant>> = LazyLock::new(|| DashMap::new());
impl OmikronConnection {
pub async fn send_ping(&self) {
let id = rand_u32();
PING_TIMES.insert(id, Instant::now());
PING_TIMES.retain(|_, v| v.elapsed() < Duration::from_secs(30));
let ping_message = CommunicationValue::new(CommunicationType::ping)
.with_id(id)
.add_data(
DataTypes::last_ping,
DataValue::Array(vec![DataValue::Number(*self.last_ping.lock().await)]),
);
self.send_message(&ping_message).await;
}
pub async fn handle_pong(&self, cv: &CommunicationValue) {
let id = cv.get_id();
if let Some((_, send_time)) = PING_TIMES.remove(&id) {
let ping_ms = Instant::now().duration_since(send_time).as_millis() as i64;
*self.last_ping.lock().await = ping_ms;
APP_STATE.lock().unwrap().push_ping_val(ping_ms as f64);
}
}
}

View file

@ -0,0 +1,583 @@
use crate::anonymous_clients::anonymous_manager;
use crate::calls::{call_manager, call_util};
use crate::omega::omega_connection::get_omega_connection;
use crate::rho::connection::GeneralConnection;
use crate::rho::{rho_connection::RhoConnection, rho_manager};
use crate::util::logger::PrintType;
use crate::{data::user::UserStatus, omega::omega_connection::OmegaConnection};
use crate::{log_cv_in, log_cv_out, log_err, log_in, log_out};
use std::str::FromStr;
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::sync::RwLock;
use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue};
use ttp_native::{Receiver, Sender};
use uuid::Uuid;
pub struct ClientConnection {
pub user_id: u64,
pub sender: Arc<Sender>,
pub receiver: Arc<Receiver>,
pub ping: Arc<RwLock<i64>>,
pub_key: Arc<RwLock<Option<Vec<u8>>>>,
pub rho_connection: Arc<RwLock<Option<Arc<RhoConnection>>>>,
pub interested_users: Arc<RwLock<Vec<i64>>>,
is_open: Arc<RwLock<bool>>,
}
impl ClientConnection {
pub async fn from_general(general: Arc<GeneralConnection>, user_id: u64) -> Arc<Self> {
Arc::new(Self {
ping: Arc::new(RwLock::new(0)),
pub_key: Arc::new(RwLock::new(None)),
rho_connection: general.rho_connection.clone(),
interested_users: Arc::new(RwLock::new(Vec::new())),
is_open: Arc::new(RwLock::new(true)),
sender: general.sender.clone(),
receiver: general.receiver.clone(),
user_id: user_id,
})
}
pub fn start(self: Arc<Self>) {
let self_clone = self.clone();
tokio::spawn(async move {
while let Ok(cv) = self_clone.receiver.receive().await {
self_clone.clone().handle_message(cv).await;
}
self_clone.handle_close().await;
});
let self_clone2 = self.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(50)).await;
if self_clone2.get_rho_connection().await.is_none() {
self_clone2
.send_error_response(0, CommunicationType::error)
.await;
}
});
}
/// Get the user ID
pub async fn get_user_id(&self) -> u64 {
self.user_id
}
/// Get current ping
pub async fn get_ping(&self) -> i64 {
*self.ping.read().await
}
/// Get RhoConnection if available
pub async fn get_rho_connection(&self) -> Option<Arc<RhoConnection>> {
self.rho_connection.read().await.clone()
}
/// Send a CommunicationValue to the client
pub async fn send_message(self: Arc<Self>, cv: &CommunicationValue) {
if !*self.is_open.read().await {
log_out!(
self.user_id as i64,
PrintType::Client,
"Attempted to send message to a closed connection."
);
return;
}
if !cv.is_type(CommunicationType::pong) && !cv.is_type(CommunicationType::ping) {
log_cv_out!(PrintType::Client, &cv);
}
let _ = self.sender.send(&cv).await;
}
/// Handle incoming message from client
pub async fn handle_message(self: Arc<Self>, cv: CommunicationValue) {
tokio::spawn(async move {
if cv.is_type(CommunicationType::ping) {
self.handle_ping(cv).await;
return;
}
log_cv_in!(PrintType::Client, cv);
// Handle client status changes
if cv.is_type(CommunicationType::client_changed) {
self.handle_client_changed(cv).await;
return;
}
// Handle call invites
if cv.is_type(CommunicationType::call_invite) {
self.handle_call_invite(cv).await;
return;
}
// Handle get call requests
if cv.is_type(CommunicationType::call_token) {
self.handle_get_call(cv).await;
return;
}
if cv.is_type(CommunicationType::call_disconnect_user) {
self.handle_call_disconnect_user(cv).await;
return;
}
if cv.is_type(CommunicationType::call_timeout_user) {
self.handle_call_timeout_user(cv).await;
return;
}
if cv.is_type(CommunicationType::call_set_anonymous_joining) {
self.handle_call_set_anonymous_joining(cv).await;
return;
}
if cv.is_type(CommunicationType::get_user_data) {
if let Some(anonymous) = {
if let Some(user_id) = cv.get_data(DataTypes::user_id).as_number() {
anonymous_manager::get_anonymous_user(user_id as u64).await
} else if let Some(username) = cv.get_data(DataTypes::username).as_str() {
anonymous_manager::get_anonymous_user_by_name(username.to_string()).await
} else {
None
}
} {
let response = CommunicationValue::new(CommunicationType::get_user_data)
.with_id(cv.get_id())
.add_data(
DataTypes::username,
DataValue::Str(anonymous.get_user_name().await),
)
.add_data(
DataTypes::user_id,
DataValue::Number(anonymous.get_user_id() as i64),
)
.add_data(
DataTypes::display,
DataValue::Str(anonymous.get_display_name().await),
)
.add_data(
DataTypes::avatar,
DataValue::Str(anonymous.get_avatar().await),
)
.add_data(DataTypes::user_state, DataValue::Str("online".to_string()));
self.send_message(&response).await;
return;
}
}
if cv.is_type(CommunicationType::change_user_data)
|| cv.is_type(CommunicationType::read_notification)
|| cv.is_type(CommunicationType::get_notifications)
|| cv.is_type(CommunicationType::get_user_data)
|| cv.is_type(CommunicationType::get_iota_data)
|| cv.is_type(CommunicationType::delete_user)
{
let sender = self.get_user_id().await;
self.handle_omega_forward(cv.with_sender(sender as u64))
.await;
return;
}
// Forward other messages to Iota
self.forward_to_iota(cv).await;
});
}
async fn handle_omega_forward(self: Arc<Self>, cv: CommunicationValue) {
let client_for_closure = self.clone();
tokio::spawn(async move {
let response_cv = get_omega_connection()
.await_response(&cv.with_sender(self.user_id), Some(Duration::from_secs(20)))
.await;
if let Ok(response_cv) = response_cv {
client_for_closure.send_message(&response_cv).await;
}
});
}
/// Handle ping message
async fn handle_ping(self: Arc<Self>, cv: CommunicationValue) {
// Update our ping if provided
if let DataValue::Number(last_ping) = cv.get_data(DataTypes::last_ping) {
let current = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis();
let mut ping_guard = self.ping.write().await;
*ping_guard = current as i64 - last_ping;
}
// Get Iota ping from RhoConnection
let iota_ping = if let Some(rho_conn) = self.get_rho_connection().await {
rho_conn.get_iota_connection().get_ping().await
} else {
-1
};
// Send pong response
let response = CommunicationValue::new(CommunicationType::pong)
.with_id(cv.get_id())
.add_data(DataTypes::ping_iota, DataValue::Number(iota_ping));
self.send_message(&response).await;
}
/// Handle client status change
async fn handle_client_changed(self: Arc<Self>, cv: CommunicationValue) {
let user_id = self.get_user_id().await;
if let DataValue::Str(_status_str) = cv.get_data(DataTypes::user_state) {
let user_status = UserStatus::user_online;
if let Some(rho_conn) = self.get_rho_connection().await {
OmegaConnection::client_changed(
rho_conn.get_iota_id().await as i64,
user_id as i64,
user_status,
)
.await;
}
}
}
/// Handle call invite
async fn handle_call_invite(self: Arc<Self>, cv: CommunicationValue) {
let receiver_id: i64 = cv.get_data(DataTypes::receiver_id).as_number().unwrap_or(0);
if receiver_id == 0 {
self.send_error_response(cv.get_id(), CommunicationType::error_no_user_id)
.await;
return;
}
let call_id = match cv.get_data(DataTypes::call_id) {
DataValue::Str(id_str) => match Uuid::parse_str(id_str.as_str()) {
Ok(id) => id,
Err(_) => {
self.send_error_response(cv.get_id(), CommunicationType::error_invalid_call_id)
.await;
return;
}
},
_ => {
self.send_error_response(cv.get_id(), CommunicationType::error_no_call_id)
.await;
return;
}
};
let invited = call_manager::add_invite(call_id, self.user_id, receiver_id as u64).await;
if !invited {
self.send_error_response(cv.get_id(), CommunicationType::error_invalid_call_id)
.await;
return;
}
// Find target RhoConnection
let target_rho = match rho_manager::get_rho_con_for_user(receiver_id).await {
Some(rho) => rho,
_ => {
self.send_error_response(cv.get_id(), CommunicationType::error)
.await;
return;
}
};
// Get sender user ID
let sender_id = self.get_user_id().await;
// Create and send call distribution message
let forward = CommunicationValue::new(CommunicationType::call_invite)
.with_receiver(receiver_id as u64)
.with_sender(sender_id as u64)
.add_data(DataTypes::call_id, DataValue::Str(call_id.to_string()))
.add_data(
DataTypes::receiver_id,
DataValue::Str(receiver_id.to_string()),
)
.add_data(DataTypes::sender_id, DataValue::Str(sender_id.to_string()));
target_rho.message_to_client(forward).await;
let response = CommunicationValue::new(CommunicationType::success).with_id(cv.get_id());
self.send_message(&response).await;
}
/// Handle get call request
async fn handle_get_call(self: Arc<Self>, cv: CommunicationValue) {
let user_id = self.get_user_id().await;
let call_id = match cv.get_data(DataTypes::call_id) {
DataValue::Str(id_str) => match Uuid::parse_str(id_str.as_str()) {
Ok(id) => id,
Err(_) => {
self.send_error_response(cv.get_id(), CommunicationType::error)
.await;
return;
}
},
_ => {
self.send_error_response(cv.get_id(), CommunicationType::error)
.await;
return;
}
};
if let Some(token) = call_manager::get_call_token(user_id, call_id).await {
let response = CommunicationValue::new(CommunicationType::call_token)
.with_id(cv.get_id())
.with_receiver(user_id as u64)
.add_data(DataTypes::call_token, DataValue::Str(token));
self.send_message(&response).await;
} else {
self.send_error_response(cv.get_id(), CommunicationType::error)
.await;
return;
}
}
async fn handle_call_timeout_user(self: Arc<Self>, cv: CommunicationValue) {
let call_id =
Uuid::from_str(cv.get_data(DataTypes::call_id).as_str().unwrap_or("")).unwrap();
let user_id = cv.get_data(DataTypes::user_id).as_number().unwrap_or(0);
let untill = cv.get_data(DataTypes::untill).as_number().unwrap_or(0);
let call = call_manager::get_call(call_id).await;
if let Some(call) = call {
if call
.get_caller(self.get_user_id().await)
.await
.unwrap()
.has_admin()
{
let _ = call_util::remove_participant(call_id, user_id as u64).await;
call.get_caller(user_id as u64)
.await
.unwrap()
.set_timeout(untill)
.await;
}
}
}
async fn handle_call_disconnect_user(self: Arc<Self>, cv: CommunicationValue) {
let call_id =
Uuid::from_str(cv.get_data(DataTypes::call_id).as_str().unwrap_or("")).unwrap();
let user_id = cv.get_data(DataTypes::user_id).as_number().unwrap_or(0);
let call = call_manager::get_call(call_id).await;
if let Some(call) = call {
if call
.get_caller(self.get_user_id().await)
.await
.unwrap()
.has_admin()
{
call.remove_caller(user_id as u64).await;
}
}
}
async fn handle_call_set_anonymous_joining(self: Arc<Self>, cv: CommunicationValue) {
let call_id =
Uuid::from_str(cv.get_data(DataTypes::call_id).as_str().unwrap_or("")).unwrap();
let enable = cv.get_data(DataTypes::enabled).as_bool().unwrap_or(true);
let call = call_manager::get_call(call_id).await;
let mut short_link = None;
if let Some(call) = call {
if call
.get_caller(self.get_user_id().await)
.await
.unwrap()
.has_admin()
{
call.set_anonymous_joining(enable).await;
}
short_link = call.get_short_link().await;
}
let mut response_cv =
CommunicationValue::new(CommunicationType::call_set_anonymous_joining)
.with_id(cv.get_id())
.add_data(DataTypes::call_id, DataValue::Str(call_id.to_string()))
.add_data(DataTypes::enabled, DataValue::Bool(enable));
if let Some(short_link) = short_link {
response_cv = response_cv.add_data(DataTypes::link, DataValue::Str(short_link));
}
self.send_message(&response_cv).await;
}
/// Forward message to Iota
async fn forward_to_iota(self: Arc<Self>, cv: CommunicationValue) {
let sender_user_id = self.get_user_id().await;
let msg_id = cv.get_id();
let msg_type = cv.get_type();
log_in!(
sender_user_id as i64,
PrintType::Client,
"Forwarding client->iota: sender={} type={:?} id={} receiver={}",
sender_user_id,
msg_type,
msg_id,
cv.get_receiver()
);
if cv.is_type(CommunicationType::add_conversation)
&& cv
.get_data(DataTypes::chat_partner_id)
.as_number()
.is_none()
{
let chat_partner_name = cv
.get_data(DataTypes::chat_partner_name)
.as_str()
.unwrap_or("")
.to_string();
if anonymous_manager::get_anonymous_user_by_name(chat_partner_name.to_string())
.await
.is_some()
{
self.send_error_response(cv.get_id(), CommunicationType::error_anonymous)
.await;
return;
}
let load_uuid_response = get_omega_connection()
.await_response(
&CommunicationValue::new(CommunicationType::get_user_data)
.with_id(cv.clone().get_id())
.add_data(
DataTypes::username,
DataValue::Str(chat_partner_name.clone()),
),
Some(Duration::from_secs(20)),
)
.await;
let chat_partner_id = {
if let Ok(load_uuid_response) = load_uuid_response {
load_uuid_response.get_data(DataTypes::user_id).clone()
} else {
DataValue::Null
}
};
if let Some(rho_conn) = self.get_rho_connection().await {
let iota_id = rho_conn.get_iota_id().await;
log_in!(
sender_user_id as i64,
PrintType::Client,
"Resolved rho for add_conversation: sender={} -> iota_id={} id={}",
sender_user_id,
iota_id,
msg_id
);
let updated_cv = cv
.with_sender(sender_user_id as u64)
.add_data(DataTypes::chat_partner_id, chat_partner_id);
rho_conn.message_to_iota(updated_cv).await;
} else {
log_err!(
sender_user_id as i64,
PrintType::Client,
"No rho/iota mapping found for add_conversation sender={} type={:?} id={}",
sender_user_id,
msg_type,
msg_id
);
}
return;
}
if let Some(rho_conn) = self.get_rho_connection().await {
let iota_id = rho_conn.get_iota_id().await;
log_in!(
sender_user_id as i64,
PrintType::Client,
"Resolved rho for forward: sender={} -> iota_id={} type={:?} id={}",
sender_user_id,
iota_id,
msg_type,
msg_id
);
let updated_cv = cv.with_sender(sender_user_id as u64);
rho_conn.message_to_iota(updated_cv).await;
} else {
log_err!(
sender_user_id as i64,
PrintType::Client,
"No rho/iota mapping found for sender={} type={:?} id={}",
sender_user_id,
msg_type,
msg_id
);
self.send_error_response(msg_id, CommunicationType::error)
.await;
}
}
/// Send error response
async fn send_error_response(self: Arc<Self>, message_id: u32, error_type: CommunicationType) {
let error = CommunicationValue::new(error_type).with_id(message_id);
self.send_message(&error).await;
}
/// Close the connection
pub async fn close(&self) {
let mut is_open_guard = self.is_open.write().await;
if !*is_open_guard {
return;
}
*is_open_guard = false;
let _ = self.sender.close();
}
/// Set interested users list
pub async fn set_interested_users(self: Arc<Self>, interested_ids: Vec<i64>) {
let mut interested_guard = self.interested_users.write().await;
*interested_guard = interested_ids;
}
#[allow(dead_code)]
pub async fn get_interested_users(self: Arc<Self>) -> Vec<i64> {
let interested_guard = self.interested_users.read().await;
interested_guard.clone()
}
/// Check if interested in a user and send notification
#[allow(dead_code)]
pub async fn are_you_interested(self: Arc<Self>, user_id: i64) {
let interested_guard = self.clone().get_interested_users().await;
if interested_guard.contains(&user_id) {
let notification = CommunicationValue::new(CommunicationType::client_changed)
.add_data(DataTypes::user_id, DataValue::Str(user_id.to_string()))
.add_data(DataTypes::user_state, DataValue::Str("online".to_string()));
self.send_message(&notification).await;
}
}
/// Handle connection close
pub async fn handle_close(&self) {
let user_id = self.get_user_id().await;
if let Some(rho_conn) = rho_manager::get_rho_con_for_user(user_id as i64).await {
rho_conn
.close_client_connection(Arc::new(self.clone()))
.await;
}
}
}
// Implement Clone to make it easier to work with Arc<ClientConnection>
impl Clone for ClientConnection {
fn clone(&self) -> Self {
Self {
sender: Arc::clone(&self.sender),
receiver: Arc::clone(&self.receiver),
user_id: self.user_id,
ping: Arc::clone(&self.ping),
pub_key: Arc::clone(&self.pub_key),
rho_connection: Arc::clone(&self.rho_connection),
interested_users: Arc::clone(&self.interested_users),
is_open: Arc::clone(&self.is_open),
}
}
}

358
src/rho/connection.rs Executable file
View file

@ -0,0 +1,358 @@
use rand::{Rng, distributions::Alphanumeric};
use std::{sync::Arc, time::Duration};
use tokio::sync::RwLock;
use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue};
use ttp_native::{Receiver, Sender};
use crate::{
anonymous_clients::anonymous_client_connection::AnonymousClientConnection,
get_private_key, get_public_key, log_cv_in, log_cv_out, log_err, log_in, log_out,
omega::omega_connection::get_omega_connection,
rho::{
client_connection::ClientConnection, iota_connection::IotaConnection,
rho_connection::RhoConnection, rho_manager,
},
util::{
crypto_helper::{load_public_key, public_key_to_base64},
crypto_util::{DataFormat, SecurePayload},
logger::PrintType,
},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
pub enum ConnectionKind {
Client,
Iota,
AnonymousClient,
Phi,
}
pub struct GeneralConnection {
pub sender: Arc<Sender>,
pub receiver: Arc<Receiver>,
identified: Arc<RwLock<bool>>,
challenged: Arc<RwLock<bool>>,
challenge: Arc<RwLock<String>>,
connection_kind: Arc<RwLock<Option<ConnectionKind>>>,
pub rho_connection: Arc<RwLock<Option<Arc<RhoConnection>>>>,
id: Arc<RwLock<u64>>,
pub_key: Arc<RwLock<Option<Vec<u8>>>>,
}
impl GeneralConnection {
pub fn new(sender: Sender, receiver: Receiver) -> Arc<Self> {
Arc::new(Self {
sender: Arc::new(sender),
receiver: Arc::new(receiver),
identified: Arc::new(RwLock::new(false)),
challenged: Arc::new(RwLock::new(false)),
challenge: Arc::new(RwLock::new(String::new())),
connection_kind: Arc::new(RwLock::new(None)),
rho_connection: Arc::new(RwLock::new(None)),
id: Arc::new(RwLock::new(0)),
pub_key: Arc::new(RwLock::new(None)),
})
}
}
impl GeneralConnection {
pub async fn handle(self: Arc<Self>) {
log_in!(0, PrintType::General, "General connection handler started");
loop {
let cv = match self.receiver.receive().await {
Ok(v) => v,
Err(_) => {
break;
}
};
log_cv_in!(cv);
if !*self.identified.read().await {
self.handle_identification(cv).await;
continue;
}
if !*self.challenged.read().await {
self.handle_challenge_response(cv).await;
}
if *self.challenged.read().await {
let self_clone = self.clone();
tokio::spawn(async move {
self_clone.migrate().await;
});
break;
}
}
log_out!(0, PrintType::General, "General connection handler stopped");
}
async fn handle_identification(self: &Arc<Self>, cv: CommunicationValue) {
if !cv.is_type(CommunicationType::identification) {
return;
}
if let DataValue::Number(iota_id) = cv.get_data(DataTypes::iota_id) {
*self.id.write().await = *iota_id as u64;
*self.connection_kind.write().await = Some(ConnectionKind::Iota);
let get_pub_key_msg = CommunicationValue::new(CommunicationType::get_iota_data)
.add_data(DataTypes::iota_id, DataValue::Number(*iota_id));
let response_cv = get_omega_connection()
.await_response(&get_pub_key_msg, Some(Duration::from_secs(20)))
.await;
let response_cv = match response_cv {
Ok(r) => r,
Err(_) => {
return;
}
};
let base64_pub = response_cv
.get_data(DataTypes::public_key)
.as_str()
.unwrap_or("");
let pub_key = match load_public_key(base64_pub) {
Some(pk) => pk,
None => {
return;
}
};
*self.pub_key.write().await = Some(pub_key.as_bytes().to_vec());
let challenge: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(32)
.map(char::from)
.collect();
*self.challenge.write().await = challenge.clone();
*self.identified.write().await = true;
let encrypted_challenge =
SecurePayload::new(challenge.as_bytes(), DataFormat::Raw, get_private_key())
.unwrap()
.encrypt_x448(pub_key)
.unwrap()
.export(DataFormat::Base64);
let response = CommunicationValue::new(CommunicationType::challenge)
.with_id(cv.get_id())
.add_data(
DataTypes::public_key,
DataValue::Str(public_key_to_base64(&get_public_key())),
)
.add_data(DataTypes::challenge, DataValue::Str(encrypted_challenge));
log_cv_out!(response);
let _ = self.sender.send(&response).await;
} else if let DataValue::Number(user_id) = cv.get_data(DataTypes::user_id) {
*self.id.write().await = *user_id as u64;
*self.connection_kind.write().await = Some(ConnectionKind::Client);
let get_pub_key_msg = CommunicationValue::new(CommunicationType::get_user_data)
.add_data(DataTypes::user_id, DataValue::Number(*user_id));
let response_cv = get_omega_connection()
.await_response(&get_pub_key_msg, Some(Duration::from_secs(20)))
.await;
let response_cv = match response_cv {
Ok(r) => r,
Err(_) => {
return;
}
};
let base64_pub = response_cv
.get_data(DataTypes::public_key)
.as_str()
.unwrap_or("");
let pub_key = match load_public_key(base64_pub) {
Some(pk) => pk,
None => {
return;
}
};
*self.pub_key.write().await = Some(pub_key.as_bytes().to_vec());
let challenge: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(32)
.map(char::from)
.collect();
*self.challenge.write().await = challenge.clone();
*self.identified.write().await = true;
let encrypted_challenge =
SecurePayload::new(challenge.as_bytes(), DataFormat::Raw, get_private_key())
.unwrap()
.encrypt_x448(pub_key)
.unwrap()
.export(DataFormat::Base64);
let response = CommunicationValue::new(CommunicationType::challenge)
.with_id(cv.get_id())
.add_data(
DataTypes::public_key,
DataValue::Str(public_key_to_base64(&get_public_key())),
)
.add_data(DataTypes::challenge, DataValue::Str(encrypted_challenge));
log_cv_out!(response);
let _ = self.sender.send(&response).await;
}
}
async fn handle_challenge_response(self: &Arc<Self>, cv: CommunicationValue) {
let id = *self.id.read().await as i64;
if !cv.is_type(CommunicationType::challenge_response) {
return;
}
if let DataValue::Str(response) = cv.get_data(DataTypes::challenge) {
let expected = self.challenge.read().await.clone();
if *response == expected {
*self.challenged.write().await = true;
let response = CommunicationValue::new(CommunicationType::identification_response)
.with_id(cv.get_id())
.add_data(DataTypes::accepted, DataValue::Bool(true));
log_cv_out!(response);
if let Err(_) = self.sender.send(&response).await {
return;
}
} else {
log_err!(
id,
PrintType::Iota,
"Challenge response mismatch expected={} actual={}",
expected,
response
);
}
} else {
log_err!(
id,
PrintType::Iota,
"Challenge response missing challenge payload"
);
}
}
async fn migrate(self: &Arc<Self>) -> bool {
let kind = match *self.connection_kind.read().await {
Some(kind) => kind,
None => {
return false;
}
};
let id = *self.id.read().await;
match kind {
ConnectionKind::Client => {
let notify = CommunicationValue::new(CommunicationType::user_connected)
.add_data(DataTypes::user_id, DataValue::Number(id as i64));
get_omega_connection().send_message(&notify).await;
let user_id = id as i64;
let client = ClientConnection::from_general(self.clone(), id).await;
let mut rho = rho_manager::get_rho_con_for_user(user_id).await;
if rho.is_none() {
let get_user_msg = CommunicationValue::new(CommunicationType::get_user_data)
.add_data(DataTypes::user_id, DataValue::Number(user_id));
if let Ok(user_data_cv) = get_omega_connection()
.await_response(&get_user_msg, Some(Duration::from_secs(20)))
.await
{
if let DataValue::Number(iota_id) =
user_data_cv.get_data(DataTypes::iota_id)
{
if let Some(bound_rho) =
rho_manager::bind_user_to_iota(user_id, *iota_id).await
{
bound_rho.bind_user_id(user_id).await;
rho = Some(bound_rho);
}
}
}
}
*self.rho_connection.write().await = rho.clone();
if let Some(rho_conn) = rho {
rho_conn.bind_user_id(user_id).await;
rho_conn.add_client_connection(client.clone()).await;
} else {
log_err!(
user_id,
PrintType::Client,
"No RhoConnection found for user {}, client not attached to iota",
id
);
}
client.start();
}
ConnectionKind::Iota => {
let notify = CommunicationValue::new(CommunicationType::iota_connected)
.add_data(DataTypes::iota_id, DataValue::Number(id as i64));
get_omega_connection().send_message(&notify).await;
let iota = IotaConnection::from_general(self.clone(), id).await;
let rho = Arc::new(RhoConnection::new(iota.clone(), Vec::new()).await);
iota.set_rho_connection(rho.clone()).await;
rho_manager::add_rho(rho).await;
let get_iota_msg = CommunicationValue::new(CommunicationType::get_iota_data)
.add_data(DataTypes::iota_id, DataValue::Number(id as i64));
if let Ok(iota_data_cv) = get_omega_connection()
.await_response(&get_iota_msg, Some(Duration::from_secs(20)))
.await
{
if let DataValue::Array(users) = iota_data_cv.get_data(DataTypes::user_ids) {
let mut user_ids: Vec<u64> = Vec::new();
for value in users {
if let DataValue::Number(user_id) = value {
user_ids.push(*user_id as u64);
}
}
iota.set_user_ids(user_ids).await;
}
}
iota.clone().start();
}
ConnectionKind::AnonymousClient => {
let client = AnonymousClientConnection::from_general(self.clone(), id).await;
client.start();
}
ConnectionKind::Phi => {
let phi = ClientConnection::from_general(self.clone(), id).await;
phi.start();
}
}
true
}
}

461
src/rho/iota_connection.rs Executable file
View file

@ -0,0 +1,461 @@
use crate::calls::call_group::CallGroup;
use crate::calls::call_manager;
use crate::log_cv_in;
use crate::log_cv_out;
use crate::log_err;
use crate::log_in;
use crate::omega::omega_connection::get_omega_connection;
use crate::rho::connection::GeneralConnection;
use crate::util::logger::PrintType;
use dashmap::DashMap;
use std::collections::BTreeMap;
use std::{collections::HashMap, sync::Arc, time::Duration};
use tokio::sync::RwLock;
use tokio::sync::mpsc;
use ttp_core::CommunicationType;
use ttp_core::CommunicationValue;
use ttp_core::DataTypes;
use ttp_core::DataValue;
use ttp_native::Receiver;
use ttp_native::Sender;
use x448::PublicKey;
use super::{rho_connection::RhoConnection, rho_manager};
use crate::omega::omega_connection::OmegaConnection;
#[allow(dead_code)]
pub struct IotaConnection {
pub iota_id: u64,
pub sender: Arc<Sender>,
pub receiver: Arc<Receiver>,
pub user_ids: Arc<RwLock<Vec<u64>>>,
pub ping: Arc<RwLock<i64>>,
pub_key: Arc<RwLock<Option<Vec<u8>>>>,
pub waiting_tasks:
DashMap<u32, Box<dyn Fn(Arc<IotaConnection>, CommunicationValue) -> bool + Send + Sync>>,
pub rho_connection: Arc<RwLock<Option<Arc<RhoConnection>>>>,
}
impl IotaConnection {
pub async fn from_general(general: Arc<GeneralConnection>, iota_id: u64) -> Arc<Self> {
Arc::new(Self {
ping: Arc::new(RwLock::new(0)),
pub_key: Arc::new(RwLock::new(None)),
rho_connection: general.rho_connection.clone(),
user_ids: Arc::new(RwLock::new(Vec::new())),
sender: general.sender.clone(),
receiver: general.receiver.clone(),
iota_id: iota_id,
waiting_tasks: DashMap::new(),
})
}
pub fn start(self: Arc<Self>) {
let self_clone = self.clone();
tokio::spawn(async move {
loop {
match self_clone.receiver.receive().await {
Ok(cv) => {
self_clone.clone().handle_message(cv).await;
}
Err(_) => {
break;
}
}
}
self_clone.handle_close().await;
});
}
/// Get the Iota ID
pub async fn get_iota_id(&self) -> u64 {
self.iota_id
}
#[allow(dead_code)]
pub async fn get_public_key(&self) -> Option<PublicKey> {
if let Some(public_key) = self.pub_key.read().await.clone() {
PublicKey::from_bytes(&public_key)
} else {
None
}
}
/// Get the user IDs
pub async fn get_user_ids(&self) -> Vec<u64> {
self.user_ids.read().await.clone()
}
/// Replace all users linked to this iota and synchronize the attached rho mapping.
pub async fn set_user_ids(&self, user_ids: Vec<u64>) {
{
let mut guard = self.user_ids.write().await;
*guard = user_ids.clone();
}
if let Some(rho_conn) = self.get_rho_connection().await {
let user_ids_i64: Vec<i64> = user_ids.into_iter().map(|u| u as i64).collect();
rho_conn.set_user_ids(user_ids_i64).await;
}
}
pub async fn add_user_id(&self, user_id: u64) {
let mut should_sync = false;
{
let mut guard = self.user_ids.write().await;
if !guard.contains(&user_id) {
guard.push(user_id);
should_sync = true;
}
}
if should_sync {
if let Some(rho_conn) = self.get_rho_connection().await {
rho_conn.add_user_id(user_id as i64).await;
}
}
}
/// Get current ping
pub async fn get_ping(&self) -> i64 {
*self.ping.read().await
}
/// Set the RhoConnection reference
pub async fn set_rho_connection(&self, rho_connection: Arc<RhoConnection>) {
let mut rho_ref = self.rho_connection.write().await;
*rho_ref = Some(rho_connection);
}
/// Get RhoConnection if available
pub async fn get_rho_connection(&self) -> Option<Arc<RhoConnection>> {
let rho_ref = self.rho_connection.read().await;
if let Some(weak_ref) = rho_ref.as_ref() {
Some(weak_ref.clone())
} else {
None
}
}
/// Send a CommunicationValue to the Iota
pub async fn send_message(&self, cv: &CommunicationValue) {
if !cv.is_type(CommunicationType::pong) {
log_cv_out!(PrintType::Iota, cv);
}
if let Err(e) = self.sender.send(&cv).await {
log_err!(
self.iota_id as i64,
PrintType::Iota,
"Failed to send message: {:?}",
e
);
}
}
/// Handle incoming message from Iota
pub async fn handle_message(self: Arc<Self>, cv: CommunicationValue) {
let msg_id = cv.get_id();
if let Some((_, task)) = self.waiting_tasks.remove(&msg_id) {
if (task)(self.clone(), cv.clone()) {
return;
}
}
// Handle ping
if cv.is_type(CommunicationType::ping) || cv.is_type(CommunicationType::pong) {
self.handle_ping(cv).await;
return;
}
log_cv_in!(PrintType::Iota, cv);
// Handle GET_CHATS
if cv.is_type(CommunicationType::get_chats) {
self.handle_get_chats(cv).await;
return;
}
// Handle forwarding to other Iotas or clients
let receiver_id = cv.get_receiver();
if (receiver_id != 0 && !self.get_user_ids().await.contains(&(receiver_id as u64)))
|| cv.is_type(CommunicationType::message_other_iota)
|| cv.is_type(CommunicationType::send_chat)
{
self.handle_forward_message(cv).await;
return;
}
if cv.is_type(CommunicationType::change_iota_data)
|| cv.is_type(CommunicationType::push_notification)
|| cv.is_type(CommunicationType::get_user_data)
|| cv.is_type(CommunicationType::get_iota_data)
|| cv.is_type(CommunicationType::get_register)
|| cv.is_type(CommunicationType::complete_register_user)
|| cv.is_type(CommunicationType::delete_iota)
{
let sender = self.get_iota_id().await;
self.handle_omega_forward(cv.with_sender(sender as u64))
.await;
return;
}
self.forward_to_client(cv).await;
}
#[allow(dead_code)]
async fn send_error_response(&self, message_id: u32, error_type: CommunicationType) {
let error = CommunicationValue::new(error_type).with_id(message_id);
self.send_message(&error).await;
}
#[allow(dead_code)]
async fn close(&self) {
let _ = self.sender.close();
}
async fn handle_omega_forward(self: Arc<Self>, cv: CommunicationValue) {
let iota_for_closure = self.clone();
tokio::spawn(async move {
let response_cv = get_omega_connection()
.await_response(&cv.with_sender(self.iota_id), Some(Duration::from_secs(20)))
.await;
if let Ok(response_cv) = response_cv {
iota_for_closure.send_message(&response_cv).await;
}
});
}
/// Handle ping message
async fn handle_ping(&self, cv: CommunicationValue) {
if let DataValue::Number(last_ping) = cv.get_data(DataTypes::last_ping) {
if let Ok(ping_val) = last_ping.to_string().parse::<i64>() {
let mut ping_guard = self.ping.write().await;
*ping_guard = ping_val;
}
}
let client_pings = if let Some(rho_conn) = self.get_rho_connection().await {
rho_conn.get_client_pings().await
} else {
HashMap::new()
};
let pings: Vec<(DataTypes, DataValue)> = client_pings
.into_iter()
.map(|(k, v)| (DataTypes::parse(k), DataValue::Number(v)))
.collect();
let response = CommunicationValue::new(CommunicationType::pong)
.with_id(cv.get_id())
.add_data(DataTypes::ping_clients, DataValue::Container(pings));
self.send_message(&response).await;
}
/// Handle message forwarding to other Iotas
async fn handle_forward_message(&self, cv: CommunicationValue) {
let receiver_id = cv.get_receiver();
let sender_id = cv.get_sender();
let my_user_ids = self.get_user_ids().await;
log_in!(
self.iota_id as i64,
PrintType::Iota,
"Authority check: sender_id={} receiver_id={} iota_user_ids={:?} msg_type={:?} msg_id={}",
sender_id,
receiver_id,
my_user_ids,
cv.get_type(),
cv.get_id()
);
if my_user_ids.contains(&(sender_id as u64)) {
if let Some(target_rho) = rho_manager::get_rho_con_for_user(receiver_id as i64).await {
target_rho.message_to_iota(cv).await;
} else {
let error = CommunicationValue::new(CommunicationType::error_no_iota)
.with_id(cv.get_id())
.with_sender(cv.get_sender());
self.send_message(&error).await;
}
} else {
log_err!(
self.iota_id as i64,
PrintType::Iota,
"Rejected client->iota forward: sender_id={} is not authorized for this iota. Known users={:?}",
sender_id,
my_user_ids
);
self.send_message(
&CommunicationValue::new(CommunicationType::error_invalid_user_id).add_data(
DataTypes::error_type,
DataValue::Str(
"You are sending to another User without authority.".to_string(),
),
),
)
.await;
}
}
/// Handle GET_CHATS message
async fn handle_get_chats(&self, cv: CommunicationValue) {
let receiver_id = cv.get_receiver();
let mut interested_ids: Vec<i64> = Vec::new();
// ============================
// Load Calls
// ============================
let calls: Vec<Arc<CallGroup>> = call_manager::get_call_groups(receiver_id).await;
let mut invites: HashMap<i64, Vec<DataValue>> = HashMap::new();
let empty = calls.is_empty();
for call in calls {
for inviter in call.members.read().await.iter() {
let call_self = call.get_caller(receiver_id).await.unwrap();
let inviter_id = inviter.user_id;
let timeout = *call_self.timeout.read().await;
let admin = call_self.has_admin();
// Build call container
let mut call_map: BTreeMap<DataTypes, DataValue> = BTreeMap::new();
call_map.insert(DataTypes::call_id, DataValue::Str(call.call_id.to_string()));
if timeout > 0 {
call_map.insert(DataTypes::timeout, DataValue::Number(timeout as i64));
}
if admin {
call_map.insert(DataTypes::has_admin, DataValue::Bool(true));
}
let call_container = DataValue::container_from_map(&call_map);
invites
.entry(inviter_id as i64)
.or_insert_with(Vec::new)
.push(call_container);
}
}
// ============================
// Enrich Contacts
// ============================
let enriched_contacts = if empty {
match cv.get_data(DataTypes::user_ids) {
DataValue::Array(arr) => DataValue::Array(arr.clone()),
_ => DataValue::Array(vec![]),
}
} else {
let mut enriched: Vec<DataValue> = Vec::new();
if let DataValue::Array(users) = cv.get_data(DataTypes::user_ids) {
for user_val in users {
if let DataValue::Container(entries) = user_val {
let mut user_map: BTreeMap<DataTypes, DataValue> =
entries.iter().cloned().collect();
// extract user_id
if let Some(DataValue::Number(user_id)) = user_map.get(&DataTypes::user_id)
{
interested_ids.push(*user_id);
// attach calls if exists
if let Some(call_list) = invites.get(user_id) {
user_map
.insert(DataTypes::calls, DataValue::Array(call_list.clone()));
}
}
enriched.push(DataValue::container_from_map(&user_map));
}
}
}
DataValue::Array(enriched)
};
// ============================
// Notify Omega
// ============================
OmegaConnection::user_states(receiver_id as i64, interested_ids.clone()).await;
// ============================
// Notify Rho
// ============================
if let Some(rho_conn) = self.get_rho_connection().await {
rho_conn
.set_interested(receiver_id as i64, interested_ids)
.await;
}
// ============================
// Forward to client
// ============================
self.forward_to_client(cv.add_data(DataTypes::user_ids, enriched_contacts))
.await;
}
/// Forward message to client
async fn forward_to_client(&self, cv: CommunicationValue) {
if let Some(rho_conn) = self.get_rho_connection().await {
let updated_cv = cv.with_sender(self.get_iota_id().await);
rho_conn.message_to_client(updated_cv).await;
} else {
}
}
pub async fn handle_close(&self) {
if let Some(rho_conn) = self.get_rho_connection().await {
rho_conn.close_iota_connection().await;
}
}
#[allow(dead_code)]
pub async fn await_response(
self: Arc<IotaConnection>,
cv: &CommunicationValue,
timeout_duration: Option<Duration>,
) -> Result<CommunicationValue, String> {
let (tx, mut rx) = mpsc::channel(1);
let msg_id = cv.get_id();
let task_tx = tx.clone();
self.waiting_tasks.insert(
msg_id,
Box::new(move |_, response_cv| {
let inner_tx = task_tx.clone();
tokio::spawn(async move {
let _ = inner_tx.send(response_cv).await;
});
true
}),
);
self.send_message(cv).await;
let timeout = timeout_duration.unwrap_or(Duration::from_secs(10));
match tokio::time::timeout(timeout, rx.recv()).await {
Ok(Some(response_cv)) => Ok(response_cv),
Ok(_) => Err("Failed to receive response, channel was closed.".to_string()),
Err(_) => {
self.waiting_tasks.remove(&msg_id);
Err(format!(
"Request timed out after {} seconds.",
timeout.as_secs()
))
}
}
}
}
impl std::fmt::Debug for IotaConnection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("IotaConnection")
.field("iota_id", &"[async]")
.field("identified", &"[async]")
.field("ping", &"[async]")
.finish()
}
}

6
src/rho/mod.rs Normal file
View file

@ -0,0 +1,6 @@
pub mod client_connection;
pub mod connection;
pub mod iota_connection;
pub mod rho_connection;
pub mod rho_manager;
pub mod server;

199
src/rho/rho_connection.rs Normal file
View file

@ -0,0 +1,199 @@
use super::{client_connection::ClientConnection, iota_connection::IotaConnection, rho_manager};
use crate::data::user::UserStatus;
use crate::omega::omega_connection::OmegaConnection;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue};
pub struct RhoConnection {
iota_connection: Arc<IotaConnection>,
user_ids: Arc<RwLock<Vec<i64>>>,
client_connections: Arc<RwLock<Vec<Arc<ClientConnection>>>>,
}
impl RhoConnection {
/// Create a new RhoConnection
pub async fn new(iota_connection: Arc<IotaConnection>, user_ids: Vec<i64>) -> Self {
let rho_connection = Self {
iota_connection,
user_ids: Arc::new(RwLock::new(user_ids.clone())),
client_connections: Arc::new(RwLock::new(Vec::new())),
};
rho_connection
}
pub async fn get_iota_id(&self) -> u64 {
self.iota_connection.iota_id
}
pub async fn get_user_ids(&self) -> Vec<i64> {
self.user_ids.read().await.clone()
}
pub async fn set_user_ids(&self, user_ids: Vec<i64>) {
let mut guard = self.user_ids.write().await;
*guard = user_ids;
}
pub async fn add_user_id(&self, user_id: i64) {
let mut guard = self.user_ids.write().await;
if !guard.contains(&user_id) {
guard.push(user_id);
}
}
pub async fn bind_user_id(&self, user_id: i64) {
self.add_user_id(user_id).await;
self.iota_connection.add_user_id(user_id as u64).await;
}
pub fn get_iota_connection(&self) -> &Arc<IotaConnection> {
&self.iota_connection
}
pub async fn get_client_connections(&self) -> Vec<Arc<ClientConnection>> {
let connections = self.client_connections.read().await;
connections.clone()
}
/// Get client connections for a specific user
pub async fn get_client_connections_for_user(
&self,
user_id: i64,
) -> Vec<Arc<ClientConnection>> {
let connections = self.client_connections.read().await;
let mut collections = Vec::new();
for con in connections.iter() {
if con.get_user_id().await == user_id as u64 {
collections.push(con.clone());
}
}
collections
}
/// Add a client connection
#[allow(dead_code)]
pub async fn add_client_connection(&self, connection: Arc<ClientConnection>) {
let notification = CommunicationValue::new(CommunicationType::client_connected).add_data(
DataTypes::user_id,
DataValue::Number(connection.get_user_id().await as i64),
);
self.iota_connection.send_message(&notification).await;
{
let mut connections = self.client_connections.write().await;
connections.push(Arc::clone(&connection));
}
OmegaConnection::client_changed(
self.get_iota_id().await as i64,
connection.get_user_id().await as i64,
UserStatus::user_online,
)
.await;
}
/// Remove a client connection
pub async fn close_client_connection(&self, connection: Arc<ClientConnection>) {
let target_user_id = connection.get_user_id().await;
{
let mut connections = self.client_connections.write().await;
connections.retain(|con| {
futures::executor::block_on(async { con.get_user_id().await != target_user_id })
});
}
// Notify OmegaConnection
OmegaConnection::client_changed(
self.get_iota_id().await as i64,
connection.get_user_id().await as i64,
UserStatus::user_offline,
)
.await;
}
/// Close the Iota connection and all associated client connections
pub async fn close_iota_connection(&self) {
// Close all client connections
let connections = self.get_client_connections().await;
for connection in connections {
connection.close().await;
}
// Remove from manager
rho_manager::remove_rho(self.get_iota_id().await as i64).await;
// Notify OmegaConnection
OmegaConnection::close_iota(self.get_iota_id().await as i64).await;
}
/// Send message from Iota to specific client
pub async fn message_to_client(&self, cv: CommunicationValue) {
let connections = self.client_connections.read().await;
let receiver_id = cv.get_receiver();
for connection in connections.iter() {
if connection.get_user_id().await == receiver_id {
connection.clone().send_message(&cv).await;
}
}
}
/// Send message to Iota
pub async fn message_to_iota(&self, cv: CommunicationValue) {
self.iota_connection.send_message(&cv).await;
}
/// Set interested users for a specific client
pub async fn set_interested(&self, user_id: i64, interested_ids: Vec<i64>) {
let connections = self.client_connections.read().await;
for connection in connections.iter() {
let conn_user_id = connection.get_user_id().await;
if conn_user_id == user_id as u64 {
connection
.clone()
.set_interested_users(interested_ids.clone())
.await;
break;
}
}
}
/// Check if clients are interested in a user
#[allow(dead_code)]
pub async fn are_they_interested(&self, user_id: i64) {
let connections = self.client_connections.read().await;
for connection in connections.iter() {
connection.clone().are_you_interested(user_id).await;
}
}
/// Get ping information for all clients
pub async fn get_client_pings(&self) -> HashMap<String, i64> {
let connections = self.client_connections.read().await;
let mut pings = HashMap::new();
for connection in connections.iter() {
let user_id = connection.get_user_id().await;
pings.insert(user_id.to_string(), connection.get_ping().await);
}
pings
}
/// Check if this RhoConnection contains a specific user ID
#[allow(dead_code)]
pub async fn contains_user(&self, user_id: &i64) -> bool {
self.user_ids.read().await.contains(user_id)
}
/// Get count of active client connections
#[allow(dead_code)]
pub async fn client_count(&self) -> usize {
let connections = self.client_connections.read().await;
connections.len()
}
}

83
src/rho/rho_manager.rs Normal file
View file

@ -0,0 +1,83 @@
use super::rho_connection::RhoConnection;
use crate::log_in;
use crate::util::logger::PrintType;
use std::{
collections::HashMap,
sync::{Arc, LazyLock},
};
use tokio::sync::RwLock;
pub static RHO_CONNECTIONS: LazyLock<Arc<RwLock<HashMap<i64, Arc<RhoConnection>>>>> =
LazyLock::new(|| Arc::new(RwLock::new(HashMap::new())));
pub async fn get_rho_con_for_user(user_id: i64) -> Option<Arc<RhoConnection>> {
let connections = RHO_CONNECTIONS.read().await;
for rho_connection in connections.values() {
let rho_user_ids = rho_connection.get_user_ids().await;
log_in!(
user_id,
PrintType::Client,
"Comparing user IDs: {:?}",
rho_user_ids
);
if rho_user_ids.contains(&user_id) {
return Some(Arc::clone(rho_connection));
}
}
None
}
#[allow(dead_code)]
pub async fn contains_iota(iota_id: i64) -> bool {
let connections = RHO_CONNECTIONS.read().await;
connections.contains_key(&iota_id)
}
/// Bind a user ID to an already tracked iota/rho connection.
pub async fn bind_user_to_iota(user_id: i64, iota_id: i64) -> Option<Arc<RhoConnection>> {
let connections = RHO_CONNECTIONS.read().await;
if let Some(rho_connection) = connections.get(&iota_id) {
let rho = Arc::clone(rho_connection);
drop(connections);
rho.add_user_id(user_id).await;
log_in!(
user_id,
PrintType::Client,
"Bound user {} to iota {}",
user_id,
iota_id
);
Some(rho)
} else {
None
}
}
/// Remove a RhoConnection by Iota ID
pub async fn remove_rho(iota_id: i64) -> Option<Arc<RhoConnection>> {
let mut connections = RHO_CONNECTIONS.write().await;
connections.remove(&iota_id)
}
/// Add a RhoConnection to the manager
pub async fn add_rho(rho_connection: Arc<RhoConnection>) {
let mut connections = RHO_CONNECTIONS.write().await;
let iota_id = rho_connection.get_iota_id().await;
connections.insert(iota_id as i64, rho_connection);
}
/// Get a RhoConnection by Iota ID directly
#[allow(dead_code)]
pub async fn get_rho_by_iota(iota_id: i64) -> Option<Arc<RhoConnection>> {
let connections = RHO_CONNECTIONS.read().await;
connections.get(&iota_id).map(Arc::clone)
}
/// Get the count of active connections
pub async fn connection_count() -> usize {
let connections = RHO_CONNECTIONS.read().await;
connections.len()
}

25
src/rho/server.rs Normal file
View file

@ -0,0 +1,25 @@
use crate::{
log,
rho::connection::GeneralConnection,
util::{file_util::load_file_vec, logger::PrintType},
};
use ttp_native::Host;
pub async fn start(port: u16) -> Result<(), Box<dyn std::error::Error>> {
let cert_pem = load_file_vec("certs", "cert.pem").expect("Error loading Pemfile");
let key_pem = load_file_vec("certs", "key.pem").expect("Error loading Keyfile");
let mut host: Host = ttp_native::host(port, cert_pem, key_pem).await?;
log!(0, PrintType::General, "Server listening on port {}", port);
while let Some((sender, receiver)) = host.next().await {
tokio::spawn(async move {
let conn = GeneralConnection::new(sender, receiver);
conn.handle().await;
});
}
log!(0, PrintType::General, "Server stopped");
Ok(())
}

View file

@ -1,190 +0,0 @@
use crate::server::server::is_local_network;
use crate::util::config_util::CONFIG;
use actix_web::{HttpRequest, HttpResponse, Responder, web};
use serde_json::{Value, json};
use std::net::SocketAddr;
use std::sync::Arc;
pub fn api_config(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("/api")
.route("/shutdown/", web::post().to(shutdown))
.route("/reload/", web::post().to(reload))
.route("/users/add/", web::post().to(users_add))
.route("/users/remove/", web::post().to(users_remove))
.route("/users/get/", web::get().to(users_get))
.route("/communities/add/", web::post().to(communities_add))
.route("/communities/get/", web::get().to(communities_get))
.route("/settings/set/", web::post().to(settings_set))
.route("/settings/get/", web::get().to(settings_get)),
);
}
async fn settings_set(req: HttpRequest, ssl: web::Data<bool>) -> impl Responder {
if !is_allowed_req(&req, *ssl.get_ref()) {
return forbidden();
}
let key = req.headers().get("key").and_then(|v| v.to_str().ok());
let value = req.headers().get("value").and_then(|v| v.to_str().ok());
match (key, value) {
(Some(k), Some(v)) => {
let _ = CONFIG
.write()
.await
.config
.insert(&k.to_string(), v.to_string());
success()
}
_ => error(),
}
}
async fn settings_get(req: HttpRequest, ssl: web::Data<bool>) -> impl Responder {
if !is_allowed_req(&req, *ssl.get_ref()) {
return forbidden();
}
let config = CONFIG.read().await.config.clone();
let serde_config: Value = serde_json::to_value(config.to_string()).unwrap();
HttpResponse::Ok().json(serde_config)
}
async fn communities_get(req: HttpRequest, ssl: web::Data<bool>) -> impl Responder {
if !is_allowed_req(&req, *ssl.get_ref()) {
return forbidden();
}
let communities = crate::communities::community_manager::get_communities().await;
let mut list = Vec::new();
for c in communities {
let val = c.frontend().await;
let s_val: Value = serde_json::to_value(val.to_string()).unwrap();
list.push(s_val);
}
HttpResponse::Ok().json(list)
}
async fn communities_add(
req: HttpRequest,
ssl: web::Data<bool>,
payload: web::Json<Value>,
) -> impl Responder {
if !is_allowed_req(&req, *ssl.get_ref()) {
return forbidden();
}
let name = payload["name"].as_str().unwrap_or("").to_string();
let owner = payload["owner"].as_i64().unwrap_or(0);
let community = Arc::new(crate::communities::community::Community::create(name, owner).await);
crate::communities::community_manager::add_community(community).await;
success()
}
async fn users_get(req: HttpRequest, ssl: web::Data<bool>) -> impl Responder {
if !is_allowed_req(&req, *ssl.get_ref()) {
return forbidden();
}
let users = crate::users::user_manager::get_users();
let list: Vec<_> = users
.into_iter()
.map(|u| {
let val = u.frontend();
serde_json::to_value(val.to_string()).unwrap()
})
.collect();
HttpResponse::Ok().json(list)
}
async fn users_remove(
req: HttpRequest,
ssl: web::Data<bool>,
payload: web::Json<Value>,
) -> impl Responder {
if !is_allowed_req(&req, *ssl.get_ref()) {
return forbidden();
}
let uuid = payload.get("uuid").and_then(|v| v.as_i64()).unwrap_or(0);
crate::users::user_manager::remove_user(uuid);
crate::users::user_manager::save_users();
success()
}
async fn users_add(
req: HttpRequest,
ssl: web::Data<bool>,
payload: web::Json<Value>,
) -> impl Responder {
if !is_allowed_req(&req, *ssl.get_ref()) {
return forbidden();
}
let username = match payload.get("username").and_then(|v| v.as_str()) {
Some(u) => u,
_ => return error(),
};
if let (Some(user), Some(_)) = crate::users::user_manager::create_user(username).await {
let val = user.frontend();
let s_val: Value = serde_json::to_value(val.to_string()).unwrap();
HttpResponse::Ok().json(s_val)
} else {
error()
}
}
async fn shutdown(req: HttpRequest, ssl: web::Data<bool>) -> impl Responder {
if !is_allowed_req(&req, *ssl.get_ref()) {
return forbidden();
}
*crate::SHUTDOWN.write().await = true;
success()
}
async fn reload(req: HttpRequest, ssl: web::Data<bool>) -> impl Responder {
if !is_allowed_req(&req, *ssl.get_ref()) {
return forbidden();
}
*crate::SHUTDOWN.write().await = true;
*crate::RELOAD.write().await = true;
success()
}
fn forbidden() -> HttpResponse {
HttpResponse::Forbidden().body("403 Forbidden")
}
fn success() -> HttpResponse {
HttpResponse::Ok().json(json!({ "type": "success" }))
}
fn error() -> HttpResponse {
HttpResponse::Ok().json(json!({ "type": "error" }))
}
fn is_allowed(addr: SocketAddr, ssl: bool) -> bool {
is_local_network(addr.ip()) || ssl
}
fn is_allowed_req(req: &HttpRequest, ssl: bool) -> bool {
if let Some(addr) = req.peer_addr() {
is_allowed(addr, ssl)
} else {
false
}
}

View file

@ -1,3 +0,0 @@
pub mod api;
pub mod server;
pub mod web_path_parser;

View file

@ -1,169 +0,0 @@
use crate::log;
use crate::server::api::api_config;
use crate::server::web_path_parser;
use crate::util::file_util::load_file_buf;
use crate::{ACTIVE_TASKS, SHUTDOWN};
use actix_web::{App, Error, HttpRequest, HttpServer, Responder, dev::ServerHandle, web};
use actix_web_actors::ws;
use rustls::ServerConfig;
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
use std::{
error::Error as StdError,
io::{self, BufReader, ErrorKind},
net::IpAddr,
sync::Arc,
time::Duration,
};
async fn ws_handler(req: HttpRequest, stream: web::Payload) -> Result<impl Responder, Error> {
let path = req.path().to_string();
log!("WS connection from {:?}", req.peer_addr());
let session = WsSession::new(path);
ws::start(session, &req, stream)
}
use tokio::sync::oneshot;
pub async fn start(port: u16) -> bool {
let (tx, rx) = oneshot::channel::<ServerHandle>();
let _ = tokio::spawn(async move {
let server = match load_tls_config() {
Ok(Some(tls_config)) => {
log!("HTTPS (HTTP/2) Server running on 0.0.0.0:{}", port);
let _config = (*tls_config).clone();
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(true))
.configure(api_config)
.service(web::resource("/ws/{path:.*}").route(web::get().to(ws_handler)))
.default_service(web::to(web_path_parser::handle))
})
.bind(("0.0.0.0", port))
.unwrap()
.run()
}
Ok(_) => {
log!("HTTP Server running on 0.0.0.0:{}", port);
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(false))
.configure(api_config)
.service(web::resource("/ws/{path:.*}").route(web::get().to(ws_handler)))
.default_service(web::to(web_path_parser::handle))
})
.bind(("0.0.0.0", port))
.unwrap()
.run()
}
Err(e) => {
log!("TLS config error: {}", e);
return;
}
};
let server_handle = server.handle();
tx.send(server_handle).unwrap();
ACTIVE_TASKS.insert("WebServer".into());
server.await.unwrap();
ACTIVE_TASKS.remove("WebServer");
log!("Web Server shutdown complete.");
});
if let Ok(server_handle) = rx.await {
tokio::spawn(async move {
wait_for_shutdown(server_handle).await;
});
true
} else {
false
}
}
async fn wait_for_shutdown(server_handle: ServerHandle) {
loop {
if *SHUTDOWN.read().await {
log!("Shutdown signal received.");
server_handle.stop(true).await;
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
fn load_tls_config() -> Result<Option<Arc<ServerConfig>>, Box<dyn StdError>> {
let cert_file_res = load_file_buf("certs", "cert.pem");
let key_file_res = load_file_buf("certs", "cert.key");
let cert_file_buf = match cert_file_res {
Ok(b) => b,
Err(e) if e.kind() == ErrorKind::NotFound => {
log!("TLS certificate 'certs/cert.pem' not found.");
return Ok(None);
}
Err(e) => return Err(e.into()), // Other IO error
};
let key_file_buf = match key_file_res {
Ok(b) => b,
Err(e) if e.kind() == ErrorKind::NotFound => {
log!("TLS key 'certs/cert.key' not found.");
return Ok(None);
}
Err(e) => return Err(e.into()), // Other IO error
};
let cert_chain = rustls_pemfile::certs(&mut BufReader::new(cert_file_buf))
.collect::<Result<Vec<CertificateDer>, _>>()?;
// PKCS8
let mut key_reader = BufReader::new(key_file_buf);
let mut key_ders = rustls_pemfile::pkcs8_private_keys(&mut key_reader)
.map(|r| r.map(Into::into))
.collect::<Result<Vec<PrivateKeyDer>, _>>()?;
if key_ders.is_empty() {
// RSA
key_reader = BufReader::new(load_file_buf("certs", "cert.key")?); // Re-read key file
key_ders = rustls_pemfile::rsa_private_keys(&mut key_reader)
.map(|r| r.map(Into::into))
.collect::<Result<Vec<PrivateKeyDer>, _>>()?;
}
if key_ders.is_empty() {
// EC
key_reader = BufReader::new(load_file_buf("certs", "cert.key")?); // Re-read key file
key_ders = rustls_pemfile::ec_private_keys(&mut key_reader)
.map(|r| r.map(Into::into))
.collect::<Result<Vec<PrivateKeyDer>, _>>()?;
}
if key_ders.is_empty() {
return Err("No private keys found in key file. (Tried PKCS8, RSA, and EC)".into());
}
let config = ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(cert_chain, key_ders.remove(0))
.map_err(|e| io::Error::new(ErrorKind::Other, e.to_string()))?;
Ok(Some(Arc::new(config)))
}
pub fn is_local_network(addr: IpAddr) -> bool {
match addr {
IpAddr::V4(v4) => {
let o = v4.octets();
o[0] == 10
|| (o[0] == 172 && (16..=31).contains(&o[1]))
|| (o[0] == 192 && o[1] == 168)
|| o[0] == 127
|| (o[0] == 169 && o[1] == 254)
}
IpAddr::V6(v6) => {
let s = v6.segments();
(s[0] & 0xfe00) == 0xfc00 || (s[0] & 0xffc0) == 0xfe80 || v6.is_loopback()
}
}
}

View file

@ -1,77 +0,0 @@
use actix_web::{HttpRequest, HttpResponse};
use std::path::{Path, PathBuf};
use crate::util::file_util::load_file_vec;
fn codec_for_ext(ext: &str) -> &'static str {
match ext {
"html" => "text/html; charset=utf-8",
"css" => "text/css",
"js" => "application/javascript",
"json" => "application/json",
"png" => "image/png",
"ico" => "image/x-icon",
"woff2" => "font/woff2",
_ => "application/octet-stream",
}
}
pub async fn handle(req: HttpRequest) -> HttpResponse {
let req_path = req.path().trim_start_matches('/');
let mut fs_path = PathBuf::from("web");
if req_path.is_empty() {
fs_path.push("index.html");
} else {
fs_path.extend(req_path.split('/'));
}
if fs_path.is_dir() {
fs_path.push("index.html");
}
let ext_opt = fs_path.extension().and_then(|e| e.to_str());
let mut final_name = fs_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.to_string();
if ext_opt.is_none() {
if final_name.is_empty() {
final_name = "index.html".to_string();
} else {
final_name.push_str(".html");
}
}
let content_type = codec_for_ext(
fs_path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("html"),
);
let dir = fs_path.parent().unwrap_or(Path::new("web"));
match load_file_vec(dir.to_str().unwrap_or("web"), &final_name) {
Ok(content) => HttpResponse::Ok().content_type(content_type).body(content),
Err(_) => {
let ext = fs_path.extension().and_then(|e| e.to_str()).unwrap_or("");
if matches!(ext, "js" | "css" | "woff2") {
return HttpResponse::NotFound()
.content_type("text/plain")
.body("Not found");
}
let fallback = load_file_vec("web", "404.html")
.unwrap_or_else(|_| include_bytes!("../../static/web/404.html").to_vec());
HttpResponse::NotFound()
.content_type("text/html; charset=utf-8")
.body(fallback)
}
}
}

View file

@ -1,175 +0,0 @@
use ratatui::{
layout::{Alignment, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
};
use crate::terms::focus::Focus;
#[allow(mismatched_lifetime_syntaxes)]
pub fn checkbox(label: &str, checked: bool, active: bool, allowed: bool) -> Line {
let box_char = if checked { "[x]" } else { "[ ]" };
let (box_style, text_style) = if active {
if allowed {
(
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
)
} else {
(
Style::default().fg(Color::Gray),
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
)
}
} else {
(Style::default(), Style::default())
};
Line::from(vec![
Span::styled(box_char, box_style),
Span::raw(" "),
Span::styled(label, text_style),
])
}
pub fn draw_button(f: &mut ratatui::Frame, area: Rect, label: &str, style: Style) {
let p = Paragraph::new(Span::styled(label, style))
.alignment(Alignment::Center)
.block(Block::default().borders(Borders::ALL));
f.render_widget(p, area);
}
pub fn draw_buttons(
f: &mut ratatui::Frame,
area: Rect,
current_focus: Focus,
state: (bool, bool),
update_needed: bool,
downgrade_scenario: bool,
tos_or_privacy: bool,
) {
let cancel_text = if update_needed {
"[Q] Quit"
} else {
"[Q] Not now"
};
let continue_text = if downgrade_scenario {
"Downgrade"
} else {
"Continue"
};
let mut buttons = vec![
(cancel_text, Focus::Cancel),
(continue_text, Focus::Continue),
];
if tos_or_privacy {
buttons.push(("Continue with Tensamin Services", Focus::ContinueAll));
}
let padding = 2;
let min_widths: Vec<u16> = buttons
.iter()
.map(|(label, _)| label.len() as u16 + padding)
.collect();
let widths = compute_widths(area.width, &min_widths);
let mut x = area.x;
for ((label, focus), width) in buttons.iter().zip(widths) {
let chunk = Rect {
x,
y: area.y,
width,
height: area.height,
};
x += width;
let is_focused = current_focus == *focus;
let style = match focus {
Focus::Cancel => {
if is_focused {
Style::default()
.fg(Color::Black)
.bg(Color::Red)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::Red)
}
}
Focus::Continue => {
if is_focused && state.0 {
Style::default()
.fg(Color::Black)
.bg(Color::Green)
.add_modifier(Modifier::BOLD)
} else if state.0 {
Style::default().fg(Color::Green)
} else {
Style::default().fg(Color::DarkGray)
}
}
Focus::ContinueAll => {
if is_focused && state.1 {
Style::default()
.fg(Color::Black)
.bg(Color::Green)
.add_modifier(Modifier::BOLD)
} else if state.1 {
Style::default().fg(Color::Green)
} else {
Style::default().fg(Color::DarkGray)
}
}
_ => Style::default().fg(Color::DarkGray),
};
draw_button(f, chunk, label, style);
}
}
pub fn compute_widths(area_width: u16, min_widths: &[u16]) -> Vec<u16> {
let mut widths = vec![0; min_widths.len()];
let mut remaining: Vec<usize> = (0..min_widths.len()).collect();
let mut remaining_width = area_width;
while !remaining.is_empty() {
let count = remaining.len() as u16;
let equal = remaining_width / count;
let mut clamped = Vec::new();
for &i in &remaining {
if min_widths[i] > equal {
widths[i] = min_widths[i];
remaining_width -= min_widths[i];
clamped.push(i);
}
}
if clamped.is_empty() {
let mut remainder = remaining_width % count;
for &i in &remaining {
widths[i] = equal
+ if remainder > 0 {
remainder -= 1;
1
} else {
0
};
}
break;
}
remaining.retain(|i| !clamped.contains(i));
}
widths
}

View file

@ -1,491 +0,0 @@
use tokio::sync::oneshot;
use crate::{
gui::{
screens::{terms_checker::TermsCheckerScreen, terms_updater::TermsUpdaterScreen},
ui::UI,
},
terms::{
doc::Doc,
terms_getter::{Type, get_current_docs, get_newest_docs},
},
util::file_util::{load_file, save_file},
};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
pub async fn check(ui: Arc<UI>) -> (bool, bool) {
let mut state = ConsentState::load_state();
if ensure_initial_consent(ui.clone(), &mut state)
.await
.is_err()
{
return (false, false);
}
if ensure_updates(ui, &mut state).await.is_err() {
return (false, false);
};
state = state.sanitize();
state.save_state();
(state.accepted_eula, state.accepted_tos && state.accepted_pp)
}
async fn ensure_initial_consent(ui: Arc<UI>, state: &mut ConsentState) -> Result<(), ()> {
if state.accepted_eula {
return Ok(());
}
let (tx, rx) = oneshot::channel();
ui.set_screen(Box::new(TermsCheckerScreen::new(ui.clone(), Some(tx))))
.await;
let result = rx.await.unwrap_or(UserChoice::Deny);
match result {
UserChoice::AcceptEULA | UserChoice::AcceptAll => {
if let Some((eula, tos, privacy)) = get_current_docs().await {
state.accepted_eula = true;
state.eula = Some(eula);
if matches!(result, UserChoice::AcceptAll) {
state.accepted_tos = true;
state.accepted_pp = true;
state.tos = Some(tos);
state.privacy = Some(privacy);
}
}
let _ = &state.save_state();
Ok(())
}
UserChoice::Deny => Err(()),
}
}
async fn ensure_updates(ui: Arc<UI>, state: &mut ConsentState) -> Result<(), ()> {
let Some((eula_update, tos_update, privacy_update)) = get_updates().await else {
return Ok(());
};
let is_forced = matches!(eula_update, UpdateDecision::Forced(_))
|| matches!(tos_update, UpdateDecision::Forced(_))
|| matches!(privacy_update, UpdateDecision::Forced(_));
let (tx, rx) = oneshot::channel();
ui.set_screen(Box::new(TermsUpdaterScreen::new(
eula_update.clone(),
tos_update.clone(),
privacy_update.clone(),
Some(tx),
)))
.await;
let result = rx.await.unwrap_or(UserChoice::Deny);
if is_forced {
match result {
UserChoice::AcceptAll => {
state.accepted_eula = true;
state.accepted_tos = true;
state.accepted_pp = true;
}
UserChoice::AcceptEULA => {
state.accepted_eula = true;
}
UserChoice::Deny => return Err(()),
}
} else {
apply_future_updates(state, result, eula_update, tos_update, privacy_update);
}
state.save_state();
Ok(())
}
fn apply_future_updates(
state: &mut ConsentState,
result: UserChoice,
eula_update: UpdateDecision,
tos_update: UpdateDecision,
privacy_update: UpdateDecision,
) {
match result {
UserChoice::AcceptAll => {
if let UpdateDecision::Future { newest } = eula_update {
state.future_eula = Some(newest);
}
if let UpdateDecision::Future { newest } = tos_update {
state.future_tos = Some(newest);
}
if let UpdateDecision::Future { newest } = privacy_update {
state.future_privacy = Some(newest);
}
}
UserChoice::AcceptEULA => {
if let UpdateDecision::Future { newest } = eula_update {
state.future_eula = Some(newest);
}
}
UserChoice::Deny => {}
}
}
async fn get_updates() -> Option<(
// Ok(None) indicates no update
// Ok(Some) Indicates a future update
// Err indicates a update that has to be accepted before the programm can continue
UpdateDecision,
UpdateDecision,
UpdateDecision,
)> {
if let (
Some((current_eula, current_tos, current_privacy)),
Some((newest_eula, newest_tos, newest_privacy)),
) = (get_current_docs().await, get_newest_docs().await)
{
let file = load_file("", "agreements");
let accepted_state = ConsentState::from_str(&file).sanitize();
save_file("", "agreements", &accepted_state.to_string());
let eula_update: UpdateDecision = if current_eula.equals_some(&accepted_state.eula) {
if current_eula.equals(&newest_eula) {
UpdateDecision::NoChange
} else {
if newest_eula.equals_some(&accepted_state.future_eula) {
UpdateDecision::NoChange
} else {
UpdateDecision::Future {
newest: newest_eula,
}
}
}
} else if newest_eula.equals_some(&accepted_state.eula) {
UpdateDecision::NoChange
} else {
UpdateDecision::Forced(current_eula)
};
let tos_update: UpdateDecision =
if !accepted_state.accepted_tos || newest_tos.equals_some(&accepted_state.tos) {
UpdateDecision::NoChange
} else if accepted_state.accepted_tos && current_tos.equals_some(&accepted_state.tos) {
if current_tos.equals(&newest_tos) {
UpdateDecision::NoChange
} else {
if newest_tos.equals_some(&accepted_state.future_tos) {
UpdateDecision::NoChange
} else {
UpdateDecision::Future { newest: newest_tos }
}
}
} else {
UpdateDecision::Forced(current_tos)
};
let privacy_update: UpdateDecision = if !accepted_state.accepted_pp
|| newest_privacy.equals_some(&accepted_state.privacy)
{
UpdateDecision::NoChange
} else if accepted_state.accepted_pp && current_privacy.equals_some(&accepted_state.privacy)
{
if current_privacy.equals(&newest_privacy) {
UpdateDecision::NoChange
} else {
if newest_privacy.equals_some(&accepted_state.future_privacy) {
UpdateDecision::NoChange
} else {
UpdateDecision::Future {
newest: newest_privacy,
}
}
}
} else {
UpdateDecision::Forced(current_privacy)
};
match (&eula_update, &tos_update, &privacy_update) {
(&UpdateDecision::NoChange, &UpdateDecision::NoChange, &UpdateDecision::NoChange) => {
None
}
_ => Some((eula_update, tos_update, privacy_update)),
}
} else {
None
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum UserChoice {
Deny,
AcceptEULA,
AcceptAll,
}
#[derive(Debug, Clone, PartialEq)]
pub enum UpdateDecision {
NoChange,
Future { newest: Doc },
Forced(Doc),
}
#[derive(Debug, Clone)]
pub struct ConsentState {
pub eula: Option<Doc>,
pub accepted_eula: bool,
pub future_eula: Option<Doc>,
pub tos: Option<Doc>,
pub accepted_tos: bool,
pub future_tos: Option<Doc>,
pub privacy: Option<Doc>,
pub accepted_pp: bool,
pub future_privacy: Option<Doc>,
}
impl ConsentState {
fn sanitize(mut self) -> Self {
let current_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
if let Some(future_eula) = self.future_eula.clone() {
if future_eula.get_time() < current_secs {
self.eula = Some(future_eula);
self.future_eula = None;
}
}
if let Some(future_tos) = self.future_tos.clone() {
if future_tos.get_time() < current_secs {
self.tos = Some(future_tos);
self.future_tos = None;
}
}
if let Some(future_privacy) = self.future_privacy.clone() {
if future_privacy.get_time() < current_secs {
self.privacy = Some(future_privacy);
self.future_privacy = None;
}
}
if !self.accepted_eula {
self.accepted_tos = false;
self.accepted_pp = false;
}
self
}
pub fn load_state() -> ConsentState {
let file = load_file("", "agreements");
ConsentState::from_str(&file).sanitize()
}
pub fn save_state(&self) {
save_file("", "agreements", &self.to_string());
}
fn to_string(&self) -> String {
let current_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let mut file_out: String = format!(
"This file reflects the current consent state used by the application.\
\nIt may be regenerated or overwritten by the application.\
\nThis file was last edited by Tensamin at:\
\nUNIX-SECOND={}",
current_secs
);
if let Some(eula) = &self.eula {
file_out.push_str(&format!("\
\n\"EULA=true\" indicates that you read, understood and accepted Tensamin's End User Licence agreement. You can find our EULA at https://legal.tensamin.net/eula/\
\nEULA={}\
\nEULA-VERSION={}\
\nEULA-HASH={}\
", self.accepted_eula, eula.get_version(), eula.get_hash()));
if self.accepted_tos
&& let Some(tos) = &self.tos
{
file_out.push_str(&format!("\
\n\"Terms-of-Service=true\" indicates that you read, understood and accepted Tensamin's Terms of Service. You can find our Terms of Serivce at https://legal.tensamin.net/tos/\
\nTerms-of-Service={}\
\nTerms-of-Service-VERSION={}\
\nTerms-of-Service-HASH={}\
", self.accepted_tos, tos.get_version(), tos.get_hash()));
}
if self.accepted_pp
&& let Some(pp) = &self.privacy
{
file_out.push_str(&format!("\
\n\"Privacy-Policy=true\" indicates that you read, understood and accepted Tensamin's Privacy Policy. You can find our Privacy Policy at https://legal.tensamin.net/privacy/\
\nPrivacy-Policy={}\
\nPrivacy-Policy-VERSION={}\
\nPrivacy-Policy-HASH={}\
", self.accepted_pp, pp.get_version(), pp.get_hash()));
}
} else {
file_out.push_str("\
\n\"EULA=true\" indicates that you read, understood and accepted Tensamin's End User Licence Agreement. You can find Tensamin's EULA at https://legal.tensamin.net/eula/\
\nEULA=false\
");
}
if let Some(eula) = &self.future_eula {
file_out.push_str(&format!(
"\
\nFUTURE-EULA-VERSION={}\
\nFUTURE-EULA-HASH={}\
\nFUTURE-EULA-TIME={}\
",
eula.get_version(),
eula.get_hash(),
eula.get_time()
));
}
if let Some(tos) = &self.future_tos {
file_out.push_str(&format!(
"\
\nFUTURE-Terms-of-Service-VERSION={}\
\nFUTURE-Terms-of-Service-HASH={}\
\nFUTURE-Terms-of-Service-TIME={}\
",
tos.get_version(),
tos.get_hash(),
tos.get_time()
));
}
if let Some(pp) = &self.future_privacy {
file_out.push_str(&format!(
"\
\nFUTURE-Privacy-Policy-VERSION={}\
\nFUTURE-Privacy-Policy-HASH={}\
\nFUTURE-Privacy-Policy-TIME={}\
",
pp.get_version(),
pp.get_hash(),
pp.get_time()
));
}
file_out
}
fn from_str(s: &str) -> Self {
let mut eula = false;
let mut eula_version = String::new();
let mut eula_hash = String::new();
let mut pp = false;
let mut pp_version = String::new();
let mut pp_hash = String::new();
let mut tos = false;
let mut tos_version = String::new();
let mut tos_hash = String::new();
let mut future_eula_version = String::new();
let mut future_eula_hash = String::new();
let mut future_eula_time = String::new();
let mut future_tos_version = String::new();
let mut future_tos_hash = String::new();
let mut future_tos_time = String::new();
let mut future_pp_version = String::new();
let mut future_pp_hash = String::new();
let mut future_pp_time = String::new();
let mut unix = String::new();
for line in s.lines() {
if let Some(v) = line.strip_prefix("EULA=") {
eula = v == "true";
} else if let Some(v) = line.strip_prefix("EULA-VERSION=") {
eula_version = v.to_string();
} else if let Some(v) = line.strip_prefix("EULA-HASH=") {
eula_hash = v.to_string();
} else if let Some(v) = line.strip_prefix("Terms-of-Service=") {
tos = v == "true";
} else if let Some(v) = line.strip_prefix("Terms-of-Service-VERSION=") {
tos_version = v.to_string();
} else if let Some(v) = line.strip_prefix("Terms-of-Service-HASH=") {
tos_hash = v.to_string();
} else if let Some(v) = line.strip_prefix("Privacy-Policy=") {
pp = v == "true";
} else if let Some(v) = line.strip_prefix("Privacy-Policy-VERSION=") {
pp_version = v.to_string();
} else if let Some(v) = line.strip_prefix("Privacy-Policy-HASH=") {
pp_hash = v.to_string();
} else if let Some(v) = line.strip_prefix("UNIX-SECOND=") {
unix = v.to_string();
} else if let Some(v) = line.strip_prefix("FUTURE-EULA-VERSION=") {
future_eula_version = v.to_string();
} else if let Some(v) = line.strip_prefix("FUTURE-EULA-HASH=") {
future_eula_hash = v.to_string();
} else if let Some(v) = line.strip_prefix("FUTURE-EULA-TIME=") {
future_eula_time = v.to_string();
} else if let Some(v) = line.strip_prefix("FUTURE-Terms-of-Service-VERSION=") {
future_tos_version = v.to_string();
} else if let Some(v) = line.strip_prefix("FUTURE-Terms-of-Service-HASH=") {
future_tos_hash = v.to_string();
} else if let Some(v) = line.strip_prefix("FUTURE-Terms-of-Service-TIME=") {
future_tos_time = v.to_string();
} else if let Some(v) = line.strip_prefix("FUTURE-Privacy-Policy-VERSION=") {
future_pp_version = v.to_string();
} else if let Some(v) = line.strip_prefix("FUTURE-Privacy-Policy-HASH=") {
future_pp_hash = v.to_string();
} else if let Some(v) = line.strip_prefix("FUTURE-Privacy-Policy-TIME=") {
future_pp_time = v.to_string();
}
}
let unix: u64 = unix.parse::<u64>().unwrap_or(0);
let future_eula_time = future_eula_time.parse::<u64>().unwrap_or(0);
let future_tos_time = future_tos_time.parse::<u64>().unwrap_or(0);
let future_pp_time = future_pp_time.parse::<u64>().unwrap_or(0);
let state = Self {
accepted_eula: eula,
eula: Some(Doc::new(eula_version, eula_hash, Type::EULA, unix)),
accepted_pp: pp,
privacy: Some(Doc::new(pp_version, pp_hash, Type::PP, unix)),
accepted_tos: tos,
tos: Some(Doc::new(tos_version, tos_hash, Type::TOS, unix)),
future_eula: if !future_eula_version.is_empty() {
Some(Doc::new(
future_eula_version,
future_eula_hash,
Type::EULA,
future_eula_time,
))
} else {
None
},
future_tos: if !future_tos_version.is_empty() {
Some(Doc::new(
future_tos_version,
future_tos_hash,
Type::TOS,
future_tos_time,
))
} else {
None
},
future_privacy: if !future_pp_version.is_empty() {
Some(Doc::new(
future_pp_version,
future_pp_hash,
Type::PP,
future_pp_time,
))
} else {
None
},
}
.sanitize();
state
}
}

View file

@ -1,73 +0,0 @@
use json::{JsonValue, object::Object};
use crate::{terms::terms_getter::Type, util::file_util::load_file};
#[derive(Clone, Debug, PartialEq, Eq)]
#[allow(unused)]
pub struct Doc {
version: String,
hash: String,
pub doc_type: Type,
timestamp: u64,
}
#[allow(dead_code)]
impl Doc {
pub fn new(version: String, hash: String, doc_type: Type, timestamp: u64) -> Doc {
Doc {
version,
hash,
doc_type,
timestamp,
}
}
pub fn equals_some(&self, other: &Option<Self>) -> bool {
if let Some(other) = other {
self.get_version() == other.get_version() && self.get_hash() == other.get_hash()
} else {
false
}
}
pub fn equals(&self, other: &Self) -> bool {
self.get_version() == other.get_version() && self.get_hash() == other.get_hash()
}
pub fn get_version(&self) -> String {
self.version.clone()
}
pub fn get_hash(&self) -> String {
self.hash.clone()
}
pub fn get_time(&self) -> u64 {
self.timestamp.clone()
}
pub fn get_content(&self) -> String {
load_file(
format!("docs/{}/", self.doc_type.to_str()).as_str(),
format!("{}.md", self.version).as_str(),
)
}
pub fn to_json(&self) -> JsonValue {
let mut json = JsonValue::new_object();
let _ = json.insert("version", self.version.clone());
let _ = json.insert("hash", self.hash.clone());
let _ = json.insert("unix", self.timestamp.clone());
json
}
pub fn from_json(doc_type: Type, json: Object) -> Option<Self> {
let hash = json.get("hash")?.as_str()?.to_string();
let version = json.get("version")?.as_str()?.to_string();
let timestamp = json.get("unix")?.as_u64()?;
Some(Doc {
version,
hash,
doc_type,
timestamp,
})
}
}

View file

@ -1,25 +0,0 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Focus {
Eula,
Tos,
Pp,
Cancel,
Continue,
ContinueAll,
}
impl Focus {
pub fn next(&mut self, order: &[Focus]) {
if let Some(pos) = order.iter().position(|&f| f == *self) {
let next_pos = (pos + 1) % order.len();
*self = order[next_pos];
}
}
pub fn prev(&mut self, order: &[Focus]) {
if let Some(pos) = order.iter().position(|&f| f == *self) {
let prev_pos = if pos == 0 { order.len() - 1 } else { pos - 1 };
*self = order[prev_pos];
}
}
}

View file

@ -1,5 +0,0 @@
pub mod buttons;
pub mod consent_state;
pub mod doc;
pub mod focus;
pub mod terms_getter;

View file

@ -1,105 +0,0 @@
use json::JsonValue::Object;
use crate::terms::doc::Doc;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Type {
EULA,
TOS,
PP,
}
impl Type {
pub fn to_str(&self) -> &str {
match self {
Self::EULA => "eula",
Self::TOS => "tos",
Self::PP => "privacy",
}
}
pub fn to_string(&self) -> String {
match self {
Self::EULA => "End User License Agreement".to_string(),
Self::TOS => "Terms of Service".to_string(),
Self::PP => "Privacy Policy".to_string(),
}
}
}
pub fn get_link(terms_type: Type) -> String {
format!("https://legal.tensamin.net/{}/", terms_type.to_str())
}
pub fn get_newest_link(terms_type: Type) -> String {
format!("https://legal.tensamin.net/{}/newest/", terms_type.to_str())
}
pub async fn get_current_docs() -> Option<(Doc, Doc, Doc)> {
let body = reqwest::get("https://legal.tensamin.net/api/current/")
.await
.ok()?
.text()
.await
.ok()?;
let json = json::parse(&body).ok()?;
if let Object(eula) = &json["eula"] {
if let Object(tos) = &json["tos"] {
if let Object(pp) = &json["pp"] {
Some((
Doc::from_json(Type::EULA, eula.clone())?,
Doc::from_json(Type::TOS, tos.clone())?,
Doc::from_json(Type::PP, pp.clone())?,
))
} else {
None
}
} else {
None
}
} else {
None
}
}
pub async fn get_newest_docs() -> Option<(Doc, Doc, Doc)> {
let body = reqwest::get("https://legal.tensamin.net/api/newest/")
.await
.ok()?
.text()
.await
.ok()?;
let json = json::parse(&body).ok()?;
if let Object(eula) = &json["eula"] {
if let Object(tos) = &json["tos"] {
if let Object(pp) = &json["pp"] {
Some((
Doc::from_json(Type::EULA, eula.clone())?,
Doc::from_json(Type::TOS, tos.clone())?,
Doc::from_json(Type::PP, pp.clone())?,
))
} else {
None
}
} else {
None
}
} else {
None
}
}
pub async fn get_terms(terms_type: Type) -> Option<String> {
let body = reqwest::get(format!(
"https://legal.tensamin.net/api/text/{}/",
terms_type.to_str()
))
.await
.ok()?
.text()
.await
.ok()?;
Some(body)
}

View file

@ -1,61 +0,0 @@
use json::{self, JsonValue, number::Number};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone)]
pub struct Contact {
pub user_id: i64,
pub user_name: Option<String>,
pub last_message_at: Option<i64>,
}
impl Default for Contact {
fn default() -> Self {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as i64;
Contact {
user_id: 0,
user_name: None,
last_message_at: Some(now),
}
}
}
impl Contact {
pub fn new(user_id: i64) -> Self {
Contact {
user_id: user_id,
user_name: None,
last_message_at: None,
}
}
pub fn set_last_message_at(&mut self, p0: i64) {
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,
}
}
}

View file

@ -1,4 +0,0 @@
pub mod contact;
pub mod user_community_util;
pub mod user_manager;
pub mod user_profile;

View file

@ -1,67 +0,0 @@
use crate::util::file_util::save_file;
use json::{self, Array, JsonValue};
use std::fs;
use std::path::Path;
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);
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);
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/{}/communities.json", storage_owner);
Self::load_array(&file_path)
}
fn load_array(file_path: &str) -> Array {
if !Path::new(file_path).exists() {
return Array::new();
}
match fs::read_to_string(file_path) {
Ok(content) => {
let parsed = json::parse(&content);
match parsed {
Ok(JsonValue::Array(arr)) => arr,
_ => Array::new(),
}
}
Err(err) => {
eprintln!("Failed to read file {}: {}", file_path, err);
Array::new()
}
}
}
}

View file

@ -1,197 +0,0 @@
use crate::omikron::omikron_connection::OMIKRON_CONNECTION;
use crate::users::user_profile::UserProfile;
use crate::util::crypto_helper::{self, public_key_to_base64};
use crate::util::file_util::{load_file, save_file};
use crate::util::logger::PrintType;
use crate::{RELOAD, SHUTDOWN};
use crate::{log, log_cv};
use base64::{Engine as _, engine::general_purpose::STANDARD};
use hex::{self};
use json::JsonValue;
use once_cell::sync::Lazy;
use rand::Rng;
use rand_core::OsRng;
use rand_core::RngCore;
use sha2::{Digest, Sha256};
use std::io::{self};
use std::sync::Mutex;
use std::time::Duration;
use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue};
use x448::{PublicKey, Secret};
static USERS: Lazy<Mutex<Vec<UserProfile>>> = Lazy::new(|| Mutex::new(Vec::new()));
static UNIQUE: Lazy<Mutex<bool>> = Lazy::new(|| Mutex::new(false));
#[allow(dead_code)]
pub async fn load_from_tu(username: &str) -> Result<(), ()> {
let file_content = load_file("", &format!("{}.tu", username));
let segments = file_content.split("::").collect::<Vec<&str>>();
let uuid = segments[0].parse::<i64>().unwrap_or(0);
let b64_private_key = segments[1];
let secret: Secret = crypto_helper::load_secret_key(b64_private_key).unwrap();
let public_key = PublicKey::from(&secret);
let mut bytes = [0u8; 192];
OsRng.fill(bytes.as_mut());
let reset_token = STANDARD.encode(&bytes);
let user_profile = UserProfile::new(
uuid,
username.to_string(),
Some(username.to_string()),
crypto_helper::public_key_to_base64(&public_key),
crypto_helper::hex_hash(b64_private_key),
reset_token,
);
USERS.lock().unwrap().push(user_profile);
Ok(())
}
pub async fn create_user(username: &str) -> (Option<UserProfile>, Option<String>) {
let register_cv = CommunicationValue::new(CommunicationType::get_register);
let conn = OMIKRON_CONNECTION.clone();
let response_cv = match conn
.await_response(&register_cv, Some(Duration::from_secs(20)))
.await
{
Ok(cv) => cv,
Err(_) => return (None, None),
};
log_cv!(PrintType::Omega, response_cv);
let user_id = match response_cv.get_data(DataTypes::user_id).as_number() {
Some(id) => id,
None => return (None, None),
};
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(&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 = STANDARD.encode(&bytes);
let up = UserProfile::new(
user_id,
username.to_string(),
None,
STANDARD.encode(&public_key.as_bytes()),
private_key_hash,
reset_token.clone(),
);
let cv = CommunicationValue::new(CommunicationType::complete_register_user)
.add_data(DataTypes::user_id, DataValue::Number(user_id))
.add_data(DataTypes::username, DataValue::Str(username.to_string()))
.add_data(
DataTypes::public_key,
DataValue::Str(public_key_to_base64(&public_key)),
)
.add_data(DataTypes::iota_id, DataValue::Number(user_id))
.add_data(DataTypes::reset_token, DataValue::Str(reset_token));
let response_cv = conn
.await_response(&cv, Some(Duration::from_secs(20)))
.await;
if let Ok(resp) = response_cv {
log_cv!(PrintType::Omega, resp);
if !resp.is_type(CommunicationType::success) {
return (None, None);
}
} else {
return (None, None);
}
*SHUTDOWN.write().await = true;
*RELOAD.write().await = true;
log!("Created User");
save_file(
"",
&format!("{}.tu", username),
&format!("{}::{}", user_id, STANDARD.encode(&private_key.as_bytes())),
);
USERS.lock().unwrap().push(up.clone());
save_users();
(Some(up), Some(STANDARD.encode(&private_key.as_bytes())))
}
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() {
*UNIQUE.lock().unwrap() = false;
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() {
let mut users = USERS.lock().unwrap();
users.clear();
*UNIQUE.lock().unwrap() = true;
}
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);
}
}
}
if *UNIQUE.lock().unwrap() {
save_users();
}
Ok(())
}
#[allow(dead_code)]
pub fn set_unique(val: bool) {
*UNIQUE.lock().unwrap() = val;
}

View file

@ -1,126 +0,0 @@
use std::time::{SystemTime, UNIX_EPOCH};
use crate::util::file_util::{has_file, load_file, used_dir_space};
use base64::{Engine as _, engine::general_purpose};
use json::{JsonValue, object};
use rand::Rng;
use rand::rngs::OsRng;
// --- UserProfile ---
#[derive(Clone, Debug)]
pub struct UserProfile {
pub user_id: i64,
pub username: String,
pub public_key: String,
pub private_key_hash: String,
pub reset_token: String,
pub created_at: i64,
pub display_name: Option<String>,
}
impl UserProfile {
pub fn new(
user_id: i64,
username: String,
display_name: Option<String>,
public_key: String,
private_key_hash: String,
reset_token: String,
) -> Self {
Self {
user_id,
username,
display_name,
public_key,
private_key_hash,
created_at: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as i64,
reset_token,
}
}
pub fn to_json(&self) -> JsonValue {
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()
};
if let Some(d) = &self.display_name {
obj["display_name"] = d.clone().into();
}
obj
}
pub fn frontend(&self) -> JsonValue {
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,
"storage" => used_dir_space(&format!("users/{}", self.user_id.to_string())),
};
if let Some(d) = &self.display_name {
obj["display_name"] = d.clone().into();
}
if has_file("", &format!("{}.tu", self.username.clone())) {
obj["tu"] = load_file("", &format!("{}.tu", self.username.clone())).into();
}
obj
}
pub async fn from_json(j: &JsonValue) -> Option<Self> {
let user_id = j["uuid"].as_i64()?;
let username = j["username"].as_str()?.to_string();
let public_key = j["public_key"].as_str()?.to_string();
let private_key_hash = j["private_key_hash"].as_str()?.to_string();
let reset_token = j["reset_token"].as_str()?.to_string();
let created_at = j["created_at"].as_i64()?;
let display_name = j["display_name"].as_str().map(|s| s.to_string());
let up = UserProfile {
user_id,
username,
display_name,
public_key,
private_key_hash,
created_at,
reset_token,
};
// 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)
}
#[allow(dead_code)]
pub fn randomize_reset_token(&mut self) -> String {
let mut bytes = [0u8; 192];
OsRng.fill(bytes.as_mut());
let new_token = general_purpose::STANDARD.encode(&bytes);
self.reset_token = new_token.clone();
new_token
}
#[allow(dead_code)]
pub fn get_display_name(&self) -> String {
self.display_name
.clone()
.unwrap_or_else(|| self.username.clone())
}
}

View file

@ -1,276 +0,0 @@
use crate::log;
use crate::util::db;
use json::{JsonValue, array, object};
use rusqlite::params;
use std::io;
use std::sync::{Arc, LazyLock, Mutex};
#[derive(PartialEq, Debug, Clone)]
pub enum MessageState {
Read,
Received,
Sent,
Sending,
}
impl MessageState {
pub fn as_str(&self) -> &'static str {
match self {
MessageState::Read => "read",
MessageState::Received => "received",
MessageState::Sent => "sent",
MessageState::Sending => "sending",
}
}
pub fn from_str(value: &str) -> Self {
match value.to_lowercase().as_str() {
"read" => MessageState::Read,
"received" => MessageState::Received,
"sent" => MessageState::Sent,
_ => MessageState::Sending,
}
}
pub fn upgrade(self, other: Self) -> Self {
if other == Self::Read || self == Self::Read {
Self::Read
} else if other == Self::Received || self == Self::Received {
Self::Received
} else if other == Self::Sent || self == Self::Sent {
Self::Sent
} else {
Self::Sending
}
}
}
// Shared DB created via helper.
// The db helper constructs the messages sqlite file and ensures PRAGMAs and schema exist.
static MESSAGES_DB: LazyLock<Arc<Mutex<rusqlite::Connection>>> = LazyLock::new(|| {
db::create_general_messages_db().expect("Failed to create or initialize general messages DB")
});
pub fn add_message(
send_time: u128,
storage_owner_is_sender: bool,
storage_owner: i64,
external_user: i64,
message: &str,
height: i64,
) {
let message_time = match i64::try_from(send_time) {
Ok(v) => v,
Err(_) => {
log!("Failed to store message: send_time out of range for i64 ({send_time})");
return;
}
};
// Insert the message into the DB
let insert_result = db::with_conn(&MESSAGES_DB, |conn| {
conn.execute(
r#"
INSERT INTO messages (
storage_owner,
external_user,
message_time,
content,
sent_by_self,
message_state,
height
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
"#,
params![
storage_owner,
external_user,
message_time,
message,
if storage_owner_is_sender {
1_i64
} else {
0_i64
},
MessageState::Sending.as_str(),
height,
],
)?;
Ok(())
});
if let Err(e) = insert_result {
log!("Failed to insert message into sqlite: {}", e);
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);
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);
}
pub fn change_message_state(
timestamp: i64,
storage_owner: i64,
external_user: i64,
new_state: MessageState,
) -> io::Result<()> {
// Run the SELECT and UPDATE inside with_conn to centralize connection access.
let res: Result<(), String> = db::with_conn(&MESSAGES_DB, |conn| {
let current: Option<String> = match conn.query_row(
r#"
SELECT message_state
FROM messages
WHERE storage_owner = ?1
AND external_user = ?2
AND message_time = ?3
ORDER BY id DESC
LIMIT 1
"#,
params![storage_owner, external_user, timestamp],
|row| row.get(0),
) {
Ok(state) => Some(state),
Err(rusqlite::Error::QueryReturnedNoRows) => None,
Err(e) => return Err(e),
};
let Some(current_state_raw) = current else {
return Ok(());
};
let upgraded = MessageState::from_str(&current_state_raw)
.upgrade(new_state)
.as_str()
.to_string();
conn.execute(
r#"
UPDATE messages
SET message_state = ?1
WHERE id = (
SELECT id
FROM messages
WHERE storage_owner = ?2
AND external_user = ?3
AND message_time = ?4
ORDER BY id DESC
LIMIT 1
)
"#,
params![upgraded, storage_owner, external_user, timestamp],
)?;
Ok(())
});
match res {
Ok(_) => Ok(()),
Err(e) => Err(io::Error::new(io::ErrorKind::Other, e)),
}
}
pub fn get_messages(
storage_owner: i64,
external_user: i64,
loaded_messages: i64,
amount: i64,
) -> JsonValue {
let messages = array![];
if amount <= 0 || loaded_messages < 0 {
return messages;
}
let res: Result<JsonValue, String> = db::with_conn(&MESSAGES_DB, |conn| {
let mut stmt = conn.prepare(
r#"
SELECT
message_time,
content,
sent_by_self,
message_state,
height
FROM messages
WHERE storage_owner = ?1
AND external_user = ?2
ORDER BY message_time DESC, id DESC
LIMIT ?3 OFFSET ?4
"#,
)?;
let rows = stmt.query_map(
params![storage_owner, external_user, amount, loaded_messages],
|row| {
let message_time: i64 = row.get(0)?;
let content: String = row.get(1)?;
let sent_by_self: i64 = row.get(2)?;
let message_state: String = row.get(3)?;
let height: i64 = row.get(4).unwrap_or(0);
Ok((message_time, content, sent_by_self, message_state, height))
},
)?;
let mut out = array![];
for row in rows {
match row {
Ok((message_time, content, sent_by_self, message_state, height)) => {
let msg = object! {
"message_time" => message_time,
"content" => content,
"sent_by_self" => (sent_by_self != 0),
"message_state" => message_state,
"height" => height
};
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 {
Ok(v) => v,
Err(e) => {
log!("Failed to query messages: {}", e);
messages
}
}
}
#[cfg(test)]
mod tests {
use super::MessageState;
#[test]
fn upgrade_prefers_highest_state() {
assert_eq!(
MessageState::Sending.upgrade(MessageState::Sent),
MessageState::Sent
);
assert_eq!(
MessageState::Sent.upgrade(MessageState::Received),
MessageState::Received
);
assert_eq!(
MessageState::Received.upgrade(MessageState::Read),
MessageState::Read
);
}
#[test]
fn from_str_is_case_insensitive() {
assert_eq!(MessageState::from_str("READ"), MessageState::Read);
assert_eq!(MessageState::from_str("received"), MessageState::Received);
assert_eq!(MessageState::from_str("Sent"), MessageState::Sent);
assert_eq!(MessageState::from_str("unknown"), MessageState::Sending);
}
}

View file

@ -1,121 +0,0 @@
use crate::users::contact::Contact;
use crate::util::db;
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) {
if let Err(e) = db::with_conn(&MESSAGES_DB, |conn| {
conn.execute(
r#"
INSERT INTO contacts (
storage_owner,
user_id,
user_name,
last_message_at
) VALUES (?1, ?2, ?3, ?4)
ON CONFLICT(storage_owner, user_id) DO UPDATE SET
user_name = excluded.user_name,
last_message_at = excluded.last_message_at
"#,
params![
storage_owner,
contact.user_id,
contact.user_name.clone(),
contact.last_message_at
],
)?;
Ok(())
}) {
eprintln!("Failed to mod_user: {}", e);
}
}
/// Retrieve a single contact for storage_owner/user_id.
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 conn.query_row(
r#"
SELECT user_id, user_name, last_message_at
FROM contacts
WHERE storage_owner = ?1 AND user_id = ?2
LIMIT 1
"#,
params![storage_owner, user_id],
|r| {
let user_id: i64 = r.get(0)?;
let user_name: Option<String> = r.get(1)?;
let last_message_at: Option<i64> = r.get(2)?;
Ok(Contact {
user_id,
user_name,
last_message_at,
})
},
) {
Ok(c) => Ok(Some(c)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e),
}
});
match res {
Ok(opt) => opt,
Err(e) => {
eprintln!("Error querying user in get_user: {}", e);
None
}
}
}
/// 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> {
let contacts_out = Vec::new();
let res: Result<Vec<Contact>, String> = db::with_conn(&MESSAGES_DB, |conn| {
let mut stmt = conn.prepare(
r#"
SELECT user_id, user_name, last_message_at
FROM contacts
WHERE storage_owner = ?1
ORDER BY
CASE WHEN last_message_at IS NULL THEN 1 ELSE 0 END,
last_message_at DESC,
user_id ASC
"#,
)?;
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 {
user_id,
user_name,
last_message_at,
})
})?;
let mut out = Vec::new();
for row in rows {
match row {
Ok(contact) => out.push(contact),
Err(e) => eprintln!("Failed to read contact row: {}", e),
}
}
Ok(out)
});
match res {
Ok(v) => v,
Err(e) => {
eprintln!("Failed to query contacts in get_users: {}", e);
contacts_out
}
}
}

View file

@ -1,90 +0,0 @@
use crate::util::db;
use json::Array;
use rusqlite::params;
use std::sync::{Arc, LazyLock, Mutex};
static MESSAGES_DB: LazyLock<Arc<Mutex<rusqlite::Connection>>> = LazyLock::new(|| {
db::create_general_messages_db().expect("Failed to create or initialize general messages DB")
});
pub struct CommunitiesUtil;
impl CommunitiesUtil {
pub fn add_community(storage_owner: i64, address: String, title: String, position: String) {
if let Err(e) = db::with_conn(&MESSAGES_DB, |conn| {
conn.execute(
r#"
INSERT INTO communities (
storage_owner,
address,
title,
position
) VALUES (?1, ?2, ?3, ?4)
ON CONFLICT(storage_owner, address) DO UPDATE SET
title = excluded.title,
position = excluded.position
"#,
params![storage_owner, address, title, position],
)?;
Ok(())
}) {
eprintln!("Failed to add_community: {}", e);
}
}
pub fn remove_community(storage_owner: i64, community_address: String) {
if let Err(e) = db::with_conn(&MESSAGES_DB, |conn| {
conn.execute(
"DELETE FROM communities WHERE storage_owner = ?1 AND address = ?2",
params![storage_owner, community_address],
)?;
Ok(())
}) {
eprintln!("Failed to remove_community: {}", e);
}
}
pub fn get_communities(storage_owner: i64) -> Array {
let communities_out = Array::new();
let res: Result<Array, String> = db::with_conn(&MESSAGES_DB, |conn| {
let mut stmt = conn.prepare(
r#"
SELECT address, title, position
FROM communities
WHERE storage_owner = ?1
"#,
)?;
let rows = stmt.query_map(params![storage_owner], |r| {
let address: String = r.get(0)?;
let title: String = r.get(1)?;
let position: String = r.get(2)?;
Ok((address, title, position))
})?;
let mut out = Array::new();
for row in rows {
match row {
Ok((address, title, position)) => {
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),
}
}
Ok(out)
});
match res {
Ok(arr) => arr,
Err(e) => {
eprintln!("Failed to query communities in get_communities: {}", e);
communities_out
}
}
}
}

View file

@ -1,60 +0,0 @@
use crate::util::file_util::{load_file, save_file};
use json::JsonValue;
use once_cell::sync::Lazy;
use tokio::sync::RwLock;
pub static CONFIG: Lazy<RwLock<ConfigUtil>> = Lazy::new(|| RwLock::new(ConfigUtil::new()));
pub struct ConfigUtil {
pub config: JsonValue,
pub unique: bool,
}
impl ConfigUtil {
pub fn new() -> Self {
Self {
config: JsonValue::new_object(),
unique: false,
}
}
pub fn clear(&mut self) {
self.config = JsonValue::new_object();
}
pub fn load(&mut self) {
let s = load_file("", "config.json");
if !s.is_empty() {
self.config = json::parse(&s).unwrap_or(JsonValue::new_object());
}
}
pub fn get_iota_id(&self) -> i64 {
self.config["iota_id"].as_i64().unwrap_or(0)
}
pub fn get_port(&self) -> u16 {
self.config["port"].as_u16().unwrap_or(1984)
}
pub fn get_public_key(&self) -> Option<String> {
self.config["public_key"].as_str().map(String::from)
}
pub fn get_private_key(&self) -> Option<String> {
self.config["private_key"].as_str().map(String::from)
}
pub fn get(&self, key: &str) -> &JsonValue {
&self.config[key]
}
pub fn change(&mut self, key: &str, value: JsonValue) {
self.config[key] = value;
self.unique = true;
}
pub fn update(&mut self) {
if self.unique {
save_file("", "config.json", &self.config.to_string());
}
}
}

282
src/util/crypto_helper.rs Normal file → Executable file
View file

@ -1,133 +1,149 @@
use aes_gcm::{
Aes256Gcm, Nonce,
aead::{Aead, KeyInit, OsRng},
};
use base64::{Engine as _, engine::general_purpose::STANDARD};
use rand_core::RngCore;
use sha2::{Digest, Sha256};
use x448::{PublicKey, Secret, SharedSecret};
/// Errors for crypto opertions
#[derive(Debug)]
#[allow(dead_code)]
pub enum CryptoError {
Base64Decode(base64::DecodeError),
InvalidKey,
AgreementError,
EncryptionError(aes_gcm::Error),
DecryptionError(aes_gcm::Error),
}
impl From<base64::DecodeError> 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 {
STANDARD.encode(pubkey.as_bytes().as_ref())
}
pub fn secret_key_to_base64(secret: &Secret) -> String {
STANDARD.encode(secret.as_bytes().as_ref())
}
pub fn load_public_key(base64_pub: &str) -> Option<PublicKey> {
let bytes = STANDARD.decode(base64_pub).unwrap();
PublicKey::from_bytes(&bytes)
}
pub fn load_secret_key(base64_secret: &str) -> Option<Secret> {
let bytes = STANDARD.decode(base64_secret).unwrap();
Secret::from_bytes(&bytes)
}
#[allow(dead_code)]
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
}
#[allow(dead_code)]
pub fn encrypt(
base64_secret: &str,
base64_peer_pub: &str,
plaintext: &str,
) -> Result<String, CryptoError> {
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(STANDARD.encode(&out))
}
#[allow(dead_code)]
pub fn decrypt(
base64_secret: &str,
base64_peer_pub: &str,
encrypted_base64: &str,
) -> Result<String, CryptoError> {
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 = STANDARD.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<u8> {
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()
}
use aes_gcm::{
Aes256Gcm, Nonce,
aead::{Aead, KeyInit, OsRng},
};
use base64::{Engine as _, engine::general_purpose::STANDARD};
use rand_core::RngCore;
use sha2::{Digest, Sha256};
use x448::{PublicKey, Secret, SharedSecret};
/// Errors for crypto operations
#[allow(dead_code)]
#[derive(Debug)]
pub enum CryptoError {
Base64Decode(base64::DecodeError),
InvalidKey,
AgreementError,
EncryptionError(aes_gcm::Error),
DecryptionError(aes_gcm::Error),
}
impl From<base64::DecodeError> for CryptoError {
fn from(err: base64::DecodeError) -> Self {
CryptoError::Base64Decode(err)
}
}
#[allow(dead_code)]
pub struct KeyPair {
pub secret: Secret,
pub public: PublicKey,
}
#[allow(dead_code)]
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 {
STANDARD.encode(pubkey.as_bytes().as_ref())
}
pub fn secret_key_to_base64(secret: &Secret) -> String {
STANDARD.encode(secret.as_bytes().as_ref())
}
pub fn load_public_key(base64_pub: &str) -> Option<PublicKey> {
let bytes = STANDARD.decode(base64_pub).unwrap();
PublicKey::from_bytes(&bytes)
}
pub fn load_secret_key(base64_secret: &str) -> Option<Secret> {
let bytes = STANDARD.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
}
#[allow(dead_code)]
pub fn encrypt_b64(
base64_secret: &str,
base64_peer_pub: &str,
plaintext: &str,
) -> Result<String, CryptoError> {
let secret = load_secret_key(base64_secret).unwrap();
let peer_pub = load_public_key(base64_peer_pub).unwrap();
encrypt(secret, peer_pub, plaintext)
}
pub fn encrypt(
secret: Secret,
peer_pub: PublicKey,
plaintext: &str,
) -> Result<String, CryptoError> {
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(STANDARD.encode(&out))
}
pub fn decrypt_b64(
base64_secret: &str,
base64_peer_pub: &str,
encrypted_base64: &str,
) -> Result<String, CryptoError> {
let secret = load_secret_key(base64_secret).unwrap();
let peer_pub = load_public_key(base64_peer_pub).unwrap();
decrypt(secret, peer_pub, encrypted_base64)
}
pub fn decrypt(
secret: Secret,
peer_pub: PublicKey,
encrypted_base64: &str,
) -> Result<String, CryptoError> {
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 = STANDARD.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)
}
#[allow(dead_code)]
pub fn hash_it(input: &str) -> Vec<u8> {
let mut hasher = Sha256::new();
hasher.update(input.as_bytes());
hasher.finalize().to_vec()
}
#[allow(dead_code)]
pub fn hex_hash(input: &str) -> String {
let digest = hash_it(input);
digest.iter().map(|b| format!("{:02x}", b)).collect()
}

View file

@ -1,185 +0,0 @@
//! 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 crate::util::file_util::get_directory;
use rusqlite::{Connection, Error as RusqliteError};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::Duration;
/// Returns the file path for a named DB inside the application's data directory.
///
/// Arguments:
/// - `db_name` : name of the DB (without extension). Example: `"messages"`.
pub fn db_file_path(db_name: &str) -> String {
let mut p = PathBuf::from(get_directory());
p.push(format!("{db_name}.sqlite3"));
p.to_string_lossy().to_string()
}
/// Open a sqlite connection to the named DB file (no initialization).
///
/// Arguments:
/// - `db_name`: name of the DB (without extension).
pub fn open_connection(db_name: &str) -> Result<Connection, RusqliteError> {
let path = db_file_path(db_name);
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(
db_name: &str,
init_sql: &str,
) -> Result<Arc<Mutex<Connection>>, String> {
match open_and_init(db_name, init_sql) {
Ok(conn) => {
// Configure some sensible defaults for concurrency
// Attempt to set a busy timeout to reduce SQLITE_BUSY failures.
let _ = conn.busy_timeout(Duration::from_millis(250));
Ok(Arc::new(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
/// 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
F: FnOnce(&Connection) -> Result<T, RusqliteError>,
{
// 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() {
tokio::task::block_in_place(|| {
let guard = shared
.lock()
.map_err(|e| format!("DB mutex poisoned: {:?}", e))?;
f(&*guard).map_err(|e| e.to_string())
})
} else {
let guard = shared
.lock()
.map_err(|e| format!("DB mutex poisoned: {:?}", e))?;
f(&*guard).map_err(|e| e.to_string())
}
}
/// Initialize a general-purpose messages+contacts DB and return a shared
/// connection. This helper creates a single DB file that can contain multiple
/// tables (messages, contacts, ...). The SQL here is conservative and intended
/// 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(())
});
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(...)
});
*/

227
src/util/file_util.rs Executable file → Normal file
View file

@ -1,15 +1,9 @@
use reqwest::Client;
use std::ffi::OsStr;
use std::fs::{self, File};
use std::io::{self, BufReader, Read};
use std::path::{Path, PathBuf};
use sysinfo::System;
use tokio::io::AsyncWriteExt;
use uuid::Uuid;
use walkdir::WalkDir;
use zip::ZipArchive;
use crate::log;
use crate::util::logger::PrintType;
#[allow(dead_code)]
pub fn delete_directory(path: &str) -> bool {
@ -24,6 +18,8 @@ fn delete_dir_recursive(directory: &Path) -> bool {
}
if let Err(e) = fs::remove_dir_all(directory) {
log!(
0,
PrintType::General,
"[IMPORTANT] Couldn't delete directory {}: {}",
directory.display(),
e,
@ -41,6 +37,7 @@ pub fn delete_user_directory(user_id: i64) {
let _ = delete_dir_recursive(&user_dir);
}
#[allow(dead_code)]
pub fn load_file_buf(path: &str, name: &str) -> io::Result<BufReader<File>> {
let dir = Path::new(&get_directory()).join(path);
let file_path = dir.join(name);
@ -67,6 +64,7 @@ pub fn load_file_buf(path: &str, name: &str) -> io::Result<BufReader<File>> {
let file = File::open(&file_path)?;
Ok(BufReader::new(file))
}
#[allow(dead_code)]
pub fn has_file(path: &str, name: &str) -> bool {
let dir = Path::new(&get_directory()).join(path);
let file_path = dir.join(name);
@ -81,6 +79,7 @@ pub fn has_file(path: &str, name: &str) -> bool {
true
}
#[allow(dead_code)]
pub fn has_dir(path: &str) -> bool {
let dir = Path::new(&get_directory()).join(path);
@ -91,13 +90,19 @@ pub fn has_dir(path: &str) -> bool {
true
}
#[allow(dead_code)]
pub fn load_file(path: &str, name: &str) -> String {
let dir = Path::new(&get_directory()).join(path);
let file_path = dir.join(name);
if !dir.exists() {
if let Err(e) = fs::create_dir_all(&dir) {
log!("[IMPORTANT] Couldn't create directories: {}", e);
log!(
0,
PrintType::General,
"[IMPORTANT] Couldn't create directories: {}",
e
);
return String::new();
}
return String::new();
@ -105,7 +110,12 @@ pub fn load_file(path: &str, name: &str) -> String {
if !file_path.exists() {
if let Err(e) = File::create(&file_path) {
log!("[IMPORTANT] Couldn't create file: {}", e);
log!(
0,
PrintType::General,
"[IMPORTANT] Couldn't create file: {}",
e
);
}
return String::new();
}
@ -124,19 +134,27 @@ pub fn load_file_vec(path: &str, name: &str) -> Result<Vec<u8>, std::io::Error>
std::fs::read(file_path)
}
#[allow(dead_code)]
pub fn save_file(path: &str, name: &str, value: &str) {
let dir = Path::new(&get_directory()).join(path);
let file_path = dir.join(name);
if !dir.exists() {
if let Err(e) = fs::create_dir_all(&dir) {
log!("[IMPORTANT] Couldn't create directories: {}", e);
log!(
0,
PrintType::General,
"[IMPORTANT] Couldn't create directories: {}",
e
);
return;
}
}
if let Err(e) = fs::write(&file_path, value) {
log!(
0,
PrintType::General,
"[IMPORTANT] Couldn't write file {}: {}",
file_path.display(),
e
@ -144,6 +162,7 @@ pub fn save_file(path: &str, name: &str, value: &str) {
}
}
#[allow(dead_code)]
pub fn get_children(path: &str) -> Vec<String> {
let dir = Path::new(&get_directory()).join(path);
let mut children = Vec::new();
@ -164,191 +183,3 @@ pub fn get_directory() -> String {
.to_string_lossy()
.to_string()
}
// Helper to download the zip file content to a file on disk
#[allow(dead_code)]
pub fn used_space() -> u64 {
get_directory_size(&PathBuf::from(get_directory()))
}
pub fn used_dir_space(path: &str) -> u64 {
get_directory_size(&PathBuf::from(format!("{}/{}", get_directory(), path)))
}
pub fn get_directory_size(directory: &Path) -> u64 {
let mut size = 0;
for entry in WalkDir::new(directory).into_iter().filter_map(|e| e.ok()) {
let path = entry.path();
if path.is_file() {
if let Ok(metadata) = path.metadata() {
size += path.file_name().unwrap_or(OsStr::new("")).len() as u64;
size += metadata.len();
}
}
}
size
}
#[allow(dead_code)]
pub fn get_designed_storage(user_id: i64) -> String {
let user_dir = Path::new(&get_directory())
.join("users")
.join(user_id.to_string());
design_byte(get_directory_size(&user_dir))
}
#[allow(dead_code)]
pub fn design_byte(bytes: u64) -> String {
let mut hr_size = format!("{:.2}B", bytes as f64);
let k = bytes as f64 / 1024.0;
let m = k / 1024.0;
let g = m / 1024.0;
let t = g / 1024.0;
if t >= 1.0 {
hr_size = format!("{:.2}TB", t);
} else if g >= 1.0 {
hr_size = format!("{:.2}GB", g);
} else if m >= 1.0 {
hr_size = format!("{:.2}MB", m);
} else if k >= 1.0 {
hr_size = format!("{:.2}KB", k);
}
hr_size
}
#[allow(dead_code)]
pub fn get_used_ram() -> String {
let mut sys = System::new_all();
sys.refresh_all();
let used = sys.used_memory() * 1024; // kB to bytes
let total = sys.total_memory() * 1024;
format!("{}/{}", design_byte(used), design_byte(total))
}
pub async fn download_zip(url: &str, zip_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
let mut response = client.get(url).send().await?;
if !response.status().is_success() {
let err_msg = format!("Failed to download file: Status {}", response.status());
log!("{}", err_msg.clone());
return Err(err_msg.into());
}
let mut zip_file = tokio::fs::File::create(zip_path).await?;
while let Some(chunk) = response.chunk().await? {
zip_file.write_all(&chunk).await?;
}
zip_file.flush().await?;
Ok(())
}
#[allow(dead_code, deprecated)]
fn extract_zip_contents_to_folder(
zip_path: &Path,
target_dir: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let file = File::open(zip_path)?;
let mut archive = ZipArchive::new(file)?;
let staging_dir = target_dir.with_extension("staging");
let _ = fs::remove_dir_all(&staging_dir);
fs::create_dir_all(&staging_dir)?;
let mut first_item_name: Option<PathBuf> = None;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let entry_path = staging_dir.join(file.sanitized_name());
if i == 0 {
if file.name().ends_with('/') || file.sanitized_name().components().count() == 1 {
first_item_name = Some(file.sanitized_name());
}
}
if file.name().ends_with('/') {
fs::create_dir_all(&entry_path)?;
} else {
if let Some(parent) = entry_path.parent() {
fs::create_dir_all(parent)?;
}
let mut out_file = File::create(entry_path)?;
io::copy(&mut file, &mut out_file)?;
}
}
if let Some(root_path) = first_item_name {
let root_dir = staging_dir.join(&root_path);
if root_dir.is_dir() {
let root_contents_count = fs::read_dir(&staging_dir)?.count();
if root_contents_count == 1
|| (root_contents_count > 1 && fs::metadata(&root_dir).is_ok())
{
let _ = fs::remove_dir_all(target_dir);
fs::create_dir_all(target_dir)?;
for entry in fs::read_dir(root_dir)? {
let entry = entry?;
let src = entry.path();
let dest = target_dir.join(entry.file_name());
if let Err(_) = fs::rename(&src, &dest) {
if src.is_file() {
fs::copy(&src, &dest)?;
} else {
if entry.path().is_dir() {
fs::rename(&src, &dest)?;
}
}
}
}
let _ = fs::remove_dir_all(&staging_dir);
return Ok(());
}
}
}
log!("Extracting directly (no single root folder detected).");
let _ = fs::remove_dir_all(target_dir);
fs::rename(&staging_dir, target_dir)?;
Ok(())
}
#[allow(dead_code)]
pub async fn download_and_extract_zip(url: &str, as_name: &str) {
log!("Downloading ZIP file...");
let base_dir = PathBuf::from(get_directory());
let zip_filename = format!("{}.zip", Uuid::new_v4());
let zip_path = base_dir.join(&zip_filename);
let target_dir = base_dir.join(as_name);
if let Err(e) = download_zip(url, &zip_path).await {
log!("Error downloading file: {}", e);
return;
}
let zip_path_clone = zip_path.clone();
let target_dir_clone = target_dir.clone();
let extract_result = extract_zip_contents_to_folder(&zip_path_clone, &target_dir_clone);
let successful = match extract_result {
Ok(()) => true,
Err(e) => {
log!("Error during ZIP extraction: {}", e);
false
}
};
if let Err(e) = tokio::fs::remove_file(&zip_path).await {
log!("Error cleaning up ZIP file {}: {}", zip_path.display(), e);
} else if successful {
log!("Downloaded and extracted ZIP file successfully.");
}
}

242
src/util/logger.rs Executable file → Normal file
View file

@ -3,24 +3,18 @@ use std::{
fs::{self, OpenOptions},
io::Write,
path::Path,
sync::{OnceLock, atomic::Ordering, mpsc},
sync::{OnceLock, mpsc},
thread,
time::{SystemTime, UNIX_EPOCH},
};
use ratatui::style::Color;
use ansi_term::Color;
use ttp_core::{CommunicationValue, DataTypes, DataValue};
use crate::{
APP_STATE,
gui::{elements::log_card::LogEntry, ui::UNIQUE},
langu::language_manager,
};
static LOGGER: OnceLock<mpsc::Sender<LogMessage>> = OnceLock::new();
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[allow(unused)]
#[allow(dead_code)]
#[derive(Clone, Copy)]
pub enum PrintType {
Call,
Client,
@ -28,30 +22,15 @@ pub enum PrintType {
Omikron,
Omega,
General,
Command,
}
impl PrintType {
pub fn prefix_color(self) -> Color {
match self {
PrintType::Call => Color::Magenta,
PrintType::Client => Color::Green,
PrintType::Iota => Color::Yellow,
PrintType::Omikron => Color::Blue,
PrintType::Omega => Color::Cyan,
PrintType::General => Color::LightCyan,
PrintType::Command => Color::LightGreen,
}
}
}
struct LogMessage {
timestamp_ms: u128,
prefix: String,
sender: Option<i64>,
prefix: &'static str,
kind: PrintType,
is_error: bool,
translation_key: Option<String>,
format_args: Vec<String>,
message: Option<String>,
message: String,
}
pub fn startup() {
@ -75,44 +54,34 @@ pub fn startup() {
.expect("Failed to open log file");
for msg in rx {
let resolved_message = if let Some(key) = msg.translation_key {
let args: Vec<&str> = msg.format_args.iter().map(|s| s.as_str()).collect();
language_manager::format(&key, &args)
} else {
msg.message.unwrap_or_default()
let ts = fixed_box(&msg.timestamp_ms.to_string(), 13);
let sender = match msg.sender {
Some(id) => fixed_box(&id.to_string(), 19),
_ => fixed_box("", 19),
};
let timestamp = format_timestamp_inline(msg.timestamp_ms);
let entry = LogEntry::new(msg.kind, resolved_message, msg.is_error);
let line = format!("{} {} {} {}", ts, sender, msg.prefix, msg.message);
let prefix = if msg.prefix.is_empty() {
String::new()
} else {
format!("{} ", msg.prefix)
};
println!("{}", colorize(msg.kind, msg.is_error).paint(&line));
let _ = writeln!(
file,
"{} {}{}",
fixed_box(&msg.timestamp_ms.to_string(), 13),
prefix,
entry.message
);
let _ = writeln!(file, " {}", timestamp);
let mut state = APP_STATE.lock().unwrap();
state.push_log(entry.into());
let _ = writeln!(file, "{}", line);
}
});
}
fn format_timestamp_inline(timestamp_ms: u128) -> String {
let secs = (timestamp_ms / 1000) as i64;
let hours = (secs / 3600) % 24;
let minutes = (secs / 60) % 60;
let seconds = secs % 60;
format!("[{:02}:{:02}:{:02}]", hours, minutes, seconds)
fn colorize(kind: PrintType, is_error: bool) -> Color {
if is_error {
return Color::Red;
}
match kind {
PrintType::Call => Color::Purple,
PrintType::Client => Color::Green,
PrintType::Iota => Color::Yellow,
PrintType::Omikron => Color::Blue,
PrintType::Omega => Color::Cyan,
PrintType::General => Color::White,
}
}
fn fixed_box(content: &str, width: usize) -> String {
@ -125,172 +94,51 @@ fn fixed_box(content: &str, width: usize) -> String {
}
}
fn colorize(kind: PrintType, is_error: bool) -> Color {
if is_error {
return Color::Red;
}
match kind {
PrintType::Call => Color::Magenta,
PrintType::Client => Color::Green,
PrintType::Iota => Color::Yellow,
PrintType::Omikron => Color::Blue,
PrintType::Omega => Color::Cyan,
PrintType::General => Color::LightCyan,
PrintType::Command => Color::LightGreen,
}
}
pub fn log_internal_translated(
pub fn log_internal(
sender: i64,
kind: PrintType,
prefix: String,
prefix: &'static str,
is_error: bool,
key: &str,
args: Vec<String>,
message: String,
) {
let sender = if sender == 0 { None } else { Some(sender) };
if let Some(tx) = LOGGER.get() {
UNIQUE.store(true, Ordering::Relaxed);
let _ = tx.send(LogMessage {
timestamp_ms: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis(),
sender,
prefix,
kind,
is_error,
translation_key: Some(key.to_string()),
format_args: args,
message: None,
message,
});
}
}
pub fn log_internal(kind: PrintType, prefix: String, is_error: bool, message: String) {
if let Some(tx) = LOGGER.get() {
UNIQUE.store(true, Ordering::Relaxed);
let _ = tx.send(LogMessage {
timestamp_ms: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis(),
prefix,
kind,
is_error,
translation_key: None,
format_args: Vec::new(),
message: Some(message),
});
}
}
#[macro_export]
macro_rules! log_t {
($key:expr) => {
$crate::util::logger::log_internal_translated(
$crate::util::logger::PrintType::General,
"".to_string(),
false,
$key,
vec![]
)
};
($key:expr, $($arg:expr),+) => {
$crate::util::logger::log_internal_translated(
$crate::util::logger::PrintType::General,
"".to_string(),
false,
$key,
vec![$($arg),+]
)
};
}
#[macro_export]
macro_rules! log_t_err {
($key:expr) => {
$crate::util::logger::log_internal_translated(
$crate::util::logger::PrintType::General,
"".to_string(),
true,
$key,
vec![]
)
};
($key:expr, $($arg:expr),+) => {
$crate::util::logger::log_internal_translated(
$crate::util::logger::PrintType::General,
"".to_string(),
true,
$key,
vec![$($arg.to_string()),+]
)
};
}
/// Log a command message.
#[macro_export]
macro_rules! log_command {
($($arg:tt)*) => {
$crate::util::logger::log_internal(
$crate::util::logger::PrintType::Command,
"".to_string(),
false,
format!($($arg)*)
)
};
}
/// Log a general informational message.
#[macro_export]
macro_rules! log {
($($arg:tt)*) => {
$crate::util::logger::log_internal(
$crate::util::logger::PrintType::General,
"".to_string(),
false,
format!($($arg)*)
)
($sender: expr, $kind:expr, $($arg:tt)*) => {
$crate::util::logger::log_internal($sender, $kind, "", false, format!($($arg)*))
};
}
/// Log an inbound message (`>`).
#[macro_export]
macro_rules! log_in {
($($arg:tt)*) => {
$crate::util::logger::log_internal(
$crate::util::logger::PrintType::General,
">".to_string(),
false,
format!($($arg)*)
)
($sender: expr, $kind:expr, $($arg:tt)*) => {
$crate::util::logger::log_internal($sender, $kind, ">", false, format!($($arg)*))
};
}
/// Log an outbound message (`<`).
#[macro_export]
macro_rules! log_out {
($($arg:tt)*) => {
$crate::util::logger::log_internal(
$crate::util::logger::PrintType::General,
"<".to_string(),
false,
format!($($arg)*)
)
($sender:expr, $kind:expr, $($arg:tt)*) => {
$crate::util::logger::log_internal($sender, $kind, "<", false, format!($($arg)*))
};
}
/// Log an error message (`>>`).
#[macro_export]
macro_rules! log_err {
($($arg:tt)*) => {
$crate::util::logger::log_internal(
$crate::util::logger::PrintType::General,
">>".to_string(),
true,
format!($($arg)*)
)
($sender:expr, $kind:expr, $($arg:tt)*) => {
$crate::util::logger::log_internal($sender, $kind, ">>", true, format!($($arg)*))
};
}
@ -303,8 +151,9 @@ pub fn log_cv_internal(
let formatted = format_cv(cv);
log_internal(
cv.get_sender() as i64,
print_type.unwrap_or(PrintType::General),
prefix.to_string(),
prefix,
false,
formatted,
);
@ -420,7 +269,6 @@ macro_rules! log_cv_in {
$crate::util::logger::log_cv_internal("> ", &$cv, None)
};
}
#[macro_export]
macro_rules! log_cv_out {
($kind:expr, $cv:expr) => {

View file

@ -1,9 +1,4 @@
pub mod chat_files;
pub mod chats_util;
pub mod communities_util;
pub mod config_util;
pub mod crypto_helper;
pub mod crypto_util;
pub mod db;
pub mod file_util;
pub mod logger;