Added some user message handlers

Handlers now exist for user creation, auth, state messages, and post
messages. Still need to do group permissions and messages
This commit is contained in:
2026-05-26 19:47:18 -07:00
parent eaeb293915
commit d71b10f89c
7 changed files with 158 additions and 19 deletions
Generated
-1
View File
@@ -543,7 +543,6 @@ dependencies = [
[[package]] [[package]]
name = "fedichat" name = "fedichat"
version = "0.1.0" version = "0.1.0"
source = "git+https://git.firechicken.net/fedichat/fedichat-lib#a3f54705495d9aa54ffe80501e1f68876e3e9480"
dependencies = [ dependencies = [
"serde", "serde",
"serde_bytes", "serde_bytes",
+4
View File
@@ -8,6 +8,10 @@ media_directory = "/srv/confetti/media"
statefile = "./confetti.state" statefile = "./confetti.state"
loglevel = "debug" loglevel = "debug"
max_message_len_kb = 10000 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 # Optional
# account_creation_code = "password1" # account_creation_code = "password1"
@@ -10,5 +10,6 @@ CREATE TABLE messages (
signature TEXT NOT NULL, signature TEXT NOT NULL,
client_timestamp BIGINT NOT NULL, client_timestamp BIGINT NOT NULL,
server_timestamp BIGINT NOT NULL, server_timestamp BIGINT NOT NULL,
username user_t NOT NULL username user_t NOT NULL,
edited BOOLEAN DEFAULT false
) )
+147 -15
View File
@@ -1,17 +1,18 @@
use fedichat::message::{Relevance,TaggedMessage}; use fedichat::message::{Relevance,TaggedMessage};
use fedichat::client::{ClientMessage,SignedClientMessage,ServerMessage,ServerError}; use fedichat::client::{ClientMessage,SignedClientMessage,ServerMessage,ServerError};
use fedichat::state::{StatePermissionKey,StatePermissionValue};
use diesel_async::AsyncPgConnection; use diesel_async::AsyncPgConnection;
use diesel_async::pooled_connection::deadpool::{Pool,Object}; use diesel_async::pooled_connection::deadpool::{Pool,Object};
use rmp_serde; use rmp_serde;
use std::collections::HashSet; use std::collections::{HashMap,HashSet};
use std::sync::{Arc}; use std::sync::{Arc};
use thiserror::Error; use thiserror::Error;
use tokio::sync::{broadcast,mpsc,RwLock}; use tokio::sync::{broadcast,mpsc,RwLock};
use tokio::select; use tokio::select;
use crate::Coordinate; use crate::Coordinate;
use crate::db; use crate::db;
use crate::state::State; use crate::state::{self,State};
use tracing::{warn,error,debug}; use tracing::{warn,error,debug};
@@ -219,13 +220,14 @@ impl Client {
}; };
use fedichat::client::ClientMessage::*; use fedichat::client::ClientMessage::*;
let my_addr = fedichat::ServerAddr(config.hostname.clone());
// Forward the message if it is addressed to a remote server // Forward the message if it is addressed to a remote server
if let Some(servername) = message.target.clone() if let Some(servername) = message.target.clone()
&& servername != fedichat::ServerAddr(config.hostname.clone()) { && servername != fedichat::ServerAddr(config.hostname.clone()) {
if message.message.is_forwardable() { if message.message.is_forwardable() {
let user = self.get_user(config)?; 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 { } else {
Err(ServerError::MessageNotForwardable) Err(ServerError::MessageNotForwardable)
} }
@@ -264,9 +266,18 @@ impl Client {
}, },
// Private message/invite mechanism // Private message/invite mechanism
MessagePost { MessagePost {
body, // This just gets sent out ephemerally as a subscribed message
user, body: ref _body,
} => {unimplemented!()}, 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 // Replace the body of the message with a new one
MessageEdit { MessageEdit {
body, body,
@@ -278,49 +289,93 @@ impl Client {
room_id, room_id,
} => {unimplemented!()}, } => {unimplemented!()},
// State Actions // State Actions
// These don't use the DB at all
StateCreate { StateCreate {
room_id, room_id,
path, path,
content, content,
permissions, 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 { StateWrite {
room_id, room_id,
path, path,
content, 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 { StateDelete {
room_id, room_id,
path, 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 { StateAppend {
room_id, room_id,
path, path,
content, 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 { StateMove {
room_id, room_id,
path, path,
target, target,
} => {unimplemented!()}, } => {
// This could be a read and a write maybe?
unimplemented!()
},
StateRead { StateRead {
room_id, room_id,
path, 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 { PermissionAdd {
permission, permission,
path, path,
room_id 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 { PermissionRead {
path, path,
room_id 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 { PermissionDelete {
permission, permission,
path, path,
room_id 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 // Groups really should have a way to add permissions by user
// specifically for who can join or invite others // specifically for who can join or invite others
// //
@@ -462,6 +517,52 @@ impl Client {
return Ok(ServerMessage::Error(ServerError::NotAuthenticated)); return Ok(ServerMessage::Error(ServerError::NotAuthenticated));
} }
}
async fn unlock_state(
&self,
config: &crate::config::Config,
locks: &[HashSet<fedichat::state::StatePermissionKey>],
coord: &Coordinate,
user: fedichat::User
) -> Result<Vec<fedichat::state::StatePermissionKey>,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<fedichat::User,ServerError> { fn get_user(&self,config: &crate::config::Config) -> Result<fedichat::User,ServerError> {
Ok(fedichat::User { Ok(fedichat::User {
@@ -469,6 +570,37 @@ impl Client {
server: config.hostname.clone() server: config.hostname.clone()
}) })
} }
async fn run_partial_operation(
&self,
config: &crate::config::Config,
coord: &Coordinate,
user: fedichat::User,
operation: state::StateOperation
) -> Result<Option<state::StateNode>,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<ServerMessage,ServerError>
{
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<Object<AsyncPgConnection>,ServerError> { async fn get_db(&self) -> Result<Object<AsyncPgConnection>,ServerError> {
match self.db_handle.get().await { match self.db_handle.get().await {
Ok(conn) => Ok(conn), Ok(conn) => Ok(conn),
+3 -1
View File
@@ -20,7 +20,9 @@ pub struct Config {
pub loglevel: Option<String>, pub loglevel: Option<String>,
pub media_directory: String, pub media_directory: String,
pub max_message_len_kb: usize, pub max_message_len_kb: usize,
pub account_creation_code: Option<String> pub account_creation_code: Option<String>,
// Local users that get server-level permissions
pub admins: Vec<String>
} }
#[derive(Clone,Serialize,Deserialize)] #[derive(Clone,Serialize,Deserialize)]
pub struct DBConfig { pub struct DBConfig {
+1
View File
@@ -35,6 +35,7 @@ diesel::table! {
client_timestamp -> Int8, client_timestamp -> Int8,
server_timestamp -> Int8, server_timestamp -> Int8,
username -> UserT, username -> UserT,
edited -> Nullable<Bool>,
} }
} }
+1 -1
View File
@@ -780,7 +780,7 @@ impl StateNode {
} => perms } => perms
} }
} }
fn to_state_value(&self) -> fedichat::state::StateValue { pub fn to_state_value(&self) -> fedichat::state::StateValue {
match self { match self {
// Turn directory into a `ls`-like listing // Turn directory into a `ls`-like listing
StateNode::Directory { nodes, perms: _perms } => { StateNode::Directory { nodes, perms: _perms } => {