diff --git a/Cargo.lock b/Cargo.lock index 2a4cc04..bb1d864 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -543,7 +543,6 @@ dependencies = [ [[package]] name = "fedichat" version = "0.1.0" -source = "git+https://git.firechicken.net/fedichat/fedichat-lib#a3f54705495d9aa54ffe80501e1f68876e3e9480" dependencies = [ "serde", "serde_bytes", diff --git a/confetti.toml.example b/confetti.toml.example index 7253108..67ad4a3 100644 --- a/confetti.toml.example +++ b/confetti.toml.example @@ -8,6 +8,10 @@ media_directory = "/srv/confetti/media" statefile = "./confetti.state" loglevel = "debug" max_message_len_kb = 10000 +# List of users with server-level permissions that are allowed to change anything in +# any room +# admins = ["alice"] +admins = [] # Optional # account_creation_code = "password1" diff --git a/migrations/2026-05-18-182028-0000_create_messages/up.sql b/migrations/2026-05-18-182028-0000_create_messages/up.sql index 636a76f..137ff1d 100644 --- a/migrations/2026-05-18-182028-0000_create_messages/up.sql +++ b/migrations/2026-05-18-182028-0000_create_messages/up.sql @@ -10,5 +10,6 @@ CREATE TABLE messages ( signature TEXT NOT NULL, client_timestamp BIGINT NOT NULL, server_timestamp BIGINT NOT NULL, - username user_t NOT NULL + username user_t NOT NULL, + edited BOOLEAN DEFAULT false ) diff --git a/src/client.rs b/src/client.rs index 25bab64..ca1df49 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,17 +1,18 @@ use fedichat::message::{Relevance,TaggedMessage}; use fedichat::client::{ClientMessage,SignedClientMessage,ServerMessage,ServerError}; +use fedichat::state::{StatePermissionKey,StatePermissionValue}; use diesel_async::AsyncPgConnection; use diesel_async::pooled_connection::deadpool::{Pool,Object}; use rmp_serde; -use std::collections::HashSet; +use std::collections::{HashMap,HashSet}; use std::sync::{Arc}; use thiserror::Error; use tokio::sync::{broadcast,mpsc,RwLock}; use tokio::select; use crate::Coordinate; use crate::db; -use crate::state::State; +use crate::state::{self,State}; use tracing::{warn,error,debug}; @@ -219,13 +220,14 @@ impl Client { }; use fedichat::client::ClientMessage::*; + let my_addr = fedichat::ServerAddr(config.hostname.clone()); // Forward the message if it is addressed to a remote server if let Some(servername) = message.target.clone() && servername != fedichat::ServerAddr(config.hostname.clone()) { if message.message.is_forwardable() { let user = self.get_user(config)?; - self.forward(&servername,message.tag(user,fedichat::ServerAddr(config.hostname.clone()))).await + self.forward(&servername,message.tag(user,my_addr)).await } else { Err(ServerError::MessageNotForwardable) } @@ -264,9 +266,18 @@ impl Client { }, // Private message/invite mechanism MessagePost { - body, - user, - } => {unimplemented!()}, + // This just gets sent out ephemerally as a subscribed message + body: ref _body, + user: ref _other_user, + } => { + let tagged = message.tag(user,my_addr); + match self.message_send.send(tagged) { + Ok(_) => Ok(ServerMessage::Ok), + // Note: we are a receiver so there should always be at least one + // This error should never happen + Err(_e) => Err(ServerError::Generic) + } + }, // Replace the body of the message with a new one MessageEdit { body, @@ -278,49 +289,93 @@ impl Client { room_id, } => {unimplemented!()}, // State Actions + // These don't use the DB at all StateCreate { room_id, path, content, permissions, - } => {unimplemented!()}, + } => { + let mut permissions = permissions + .unwrap_or(fedichat::state::PermissionTable(HashMap::new())); + permissions.0.insert(StatePermissionKey::User(user.clone()),StatePermissionValue::Owner); + let operation = state::Operation::Create(content,permissions); + let operation = state::StateOperation::new(room_id.clone().into(),path,operation); + self.run_operation(config,&room_id.into(),user,operation).await + }, StateWrite { room_id, path, content, - } => {unimplemented!()}, + } => { + let operation = state::Operation::Write(content); + let operation = state::StateOperation::new(room_id.clone().into(),path,operation); + self.run_operation(config,&room_id.into(),user,operation).await + }, StateDelete { room_id, path, - } => {unimplemented!()}, + } => { + let operation = state::Operation::Delete; + let operation = state::StateOperation::new(room_id.clone().into(),path,operation); + self.run_operation(config,&room_id.into(),user,operation).await + }, StateAppend { room_id, path, content, - } => {unimplemented!()}, + } => { + let operation = state::Operation::Append(content); + let operation = state::StateOperation::new(room_id.clone().into(),path,operation); + self.run_operation(config,&room_id.into(),user,operation).await + }, StateMove { room_id, path, target, - } => {unimplemented!()}, + } => { + // This could be a read and a write maybe? + unimplemented!() + }, StateRead { room_id, path, - } => {unimplemented!()}, + } => { + let operation = state::Operation::Read; + let operation = state::StateOperation::new(room_id.clone().into(),path,operation); + self.run_operation(config,&room_id.into(),user,operation).await + }, PermissionAdd { permission, path, room_id - } => {unimplemented!()}, + } => { + let operation = state::Operation::PermAdd(permission); + let operation = state::StateOperation::new(room_id.clone().into(),path,operation); + self.run_operation(config,&room_id.into(),user,operation).await + }, PermissionRead { path, room_id - } => {unimplemented!()}, + } => { + let operation = state::Operation::PermRead; + let operation = state::StateOperation::new(room_id.clone().into(),path.clone(),operation); + match self.run_partial_operation(config,&room_id.into(),user,operation).await? { + Some(val) => Ok(ServerMessage::StatePermission(path,val.get_perms().clone())), + // Should never occur + None => Err(ServerError::Generic) + + } + }, PermissionDelete { permission, path, room_id - } => {unimplemented!()}, + } => { + let operation = state::Operation::PermDel(permission); + let operation = state::StateOperation::new(room_id.clone().into(),path,operation); + self.run_operation(config,&room_id.into(),user,operation).await + }, // Groups really should have a way to add permissions by user // specifically for who can join or invite others // @@ -462,6 +517,52 @@ impl Client { return Ok(ServerMessage::Error(ServerError::NotAuthenticated)); } + } + async fn unlock_state( + &self, + config: &crate::config::Config, + locks: &[HashSet], + coord: &Coordinate, + user: fedichat::User + ) -> Result,ServerError> + { + let mut allowed = HashSet::new(); + allowed.insert(StatePermissionKey::Everyone); + allowed.insert(StatePermissionKey::User(user.clone())); + if user.server == config.hostname && config.admins.contains(&user.name) { + allowed.insert(StatePermissionKey::Server); + } + if self.statehandle.read().await.get_operators(coord).await?.0.contains_key(&StatePermissionKey::User(user)) { + allowed.insert(StatePermissionKey::Operator); + } + + let mut keys = Vec::with_capacity(locks.len()); + // Operators and server admins can do whatever they want. This might need to be + // an optional sudo-like feature as it makes it very easy to accidentally delete + // important, privileged nodes + if allowed.contains(&StatePermissionKey::Server) { + for _ in locks.iter() { + keys.push(StatePermissionKey::Server); + } + } else if allowed.contains(&StatePermissionKey::Operator) { + for _ in locks.iter() { + keys.push(StatePermissionKey::Operator); + } + } else { + for lock in locks { + match allowed.intersection(&lock).next() { + Some(key) => keys.push(key.clone()), + // Try to resolve groups to see if we have any relevant permissions + // TODO, just fail for now if we would need a group perm + None => { + return Err(ServerError::MissingPermission) + } + } + + } + } + return Ok(keys); + } fn get_user(&self,config: &crate::config::Config) -> Result { Ok(fedichat::User { @@ -469,6 +570,37 @@ impl Client { server: config.hostname.clone() }) } + async fn run_partial_operation( + &self, + config: &crate::config::Config, + coord: &Coordinate, + user: fedichat::User, + operation: state::StateOperation + ) -> Result,ServerError> + { + let unlockable = self.statehandle.read().await.to_unlockable_operation(operation).await?; + let locks = unlockable.get_locks(); + let room_id = coord.clone().into(); + + let keys = self.unlock_state(config,locks,&room_id,user).await?; + let unlocked = unlockable.unlock(keys)?; + Ok(self.statehandle.write().await.execute_operation(unlocked).await?) + + } + async fn run_operation( + &self, + config: &crate::config::Config, + coord: &Coordinate, + user: fedichat::User, + operation: state::StateOperation + ) -> Result + { + let path = operation.path.clone(); + match self.run_partial_operation(config,coord,user,operation).await? { + Some(val) => Ok(ServerMessage::State(path,val.to_state_value())), + None => Ok(ServerMessage::Ok) + } + } async fn get_db(&self) -> Result,ServerError> { match self.db_handle.get().await { Ok(conn) => Ok(conn), diff --git a/src/config.rs b/src/config.rs index f7f9460..e304d8f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -20,7 +20,9 @@ pub struct Config { pub loglevel: Option, pub media_directory: String, pub max_message_len_kb: usize, - pub account_creation_code: Option + pub account_creation_code: Option, + // Local users that get server-level permissions + pub admins: Vec } #[derive(Clone,Serialize,Deserialize)] pub struct DBConfig { diff --git a/src/db/schema.rs b/src/db/schema.rs index 1e10d53..182da84 100644 --- a/src/db/schema.rs +++ b/src/db/schema.rs @@ -35,6 +35,7 @@ diesel::table! { client_timestamp -> Int8, server_timestamp -> Int8, username -> UserT, + edited -> Nullable, } } diff --git a/src/state.rs b/src/state.rs index ae32641..8a86fe1 100644 --- a/src/state.rs +++ b/src/state.rs @@ -780,7 +780,7 @@ impl StateNode { } => perms } } - fn to_state_value(&self) -> fedichat::state::StateValue { + pub fn to_state_value(&self) -> fedichat::state::StateValue { match self { // Turn directory into a `ls`-like listing StateNode::Directory { nodes, perms: _perms } => {