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]]
name = "fedichat"
version = "0.1.0"
source = "git+https://git.firechicken.net/fedichat/fedichat-lib#a3f54705495d9aa54ffe80501e1f68876e3e9480"
dependencies = [
"serde",
"serde_bytes",
+4
View File
@@ -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"
@@ -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
)
+147 -15
View File
@@ -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<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> {
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<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> {
match self.db_handle.get().await {
Ok(conn) => Ok(conn),
+3 -1
View File
@@ -20,7 +20,9 @@ pub struct Config {
pub loglevel: Option<String>,
pub media_directory: String,
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)]
pub struct DBConfig {
+1
View File
@@ -35,6 +35,7 @@ diesel::table! {
client_timestamp -> Int8,
server_timestamp -> Int8,
username -> UserT,
edited -> Nullable<Bool>,
}
}
+1 -1
View File
@@ -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 } => {