use clap::{Parser,Subcommand,ArgAction}; use ed25519_dalek::SigningKey; use fedichat::client::{ClientMessage,SignedClientMessage,AuthMethod}; use fedichat::ServerAddr; use fedichat::state::StatePath; use std::fs::File; use std::io::Read; use std::path::PathBuf; use time::OffsetDateTime; use thiserror::Error; use uuid::Uuid; // Everything except server, user, and password lives in these #[derive(Parser, Debug)] #[command(version, about, long_about = None)] pub struct Args { #[command(subcommand)] pub command: Command, #[clap(short, long)] pub server: Option, #[clap(short, long)] pub username: String, #[clap(short, long, action = ArgAction::Count)] pub verbose: u8, } #[derive(Subcommand,Debug,Clone)] pub enum Command { CreateUser { #[clap(short, long)] password: String }, // This should login and then get a token and save it. // For now because there are no tokens it saves the username // and password in cleartext :) Login { #[clap(short, long)] password: String }, SendMessage { #[clap(short, long)] body: String, // Will be parsed into the right type later #[clap(short, long, value_parser=Room::from_str)] room: Room, }, GetMessages { #[clap(short, long, default_value_t = 100)] count: u64, #[clap(value_parser=Room::from_str)] room: Room }, Join { #[clap(value_parser=Room::from_str)] room: Room, }, Leave { #[clap(value_parser=Room::from_str)] room: Room, }, Create { #[clap(value_parser=Room::from_str)] room: Room, }, Listen { #[clap(value_parser=Room::from_str)] room: Room, }, Upload { file: PathBuf, }, Fetch { #[clap(short, long)] id: String, #[clap(short, long)] file: PathBuf }, GetState { #[clap(short, long, value_parser=Room::from_str)] room: Room, // Not sure if the delimiter should be a . or a / #[clap(short, long, value_delimiter = '.', num_args = 1..)] path: Vec }, WriteState { #[clap(short, long, value_parser=Room::from_str)] room: Room, // Not sure if the delimiter should be a . or a / #[clap(short, long, value_delimiter = '.', num_args = 1..)] path: Vec, #[clap(short, long)] content: String }, SendPost { #[clap(short, long)] body: String, // Will be parsed into the right type later #[clap(short, long, value_parser=user_from_str)] user: fedichat::User, }, } impl Command { // Returns clientmessage and target server fn into_client_message(self,username: String) -> Result<(ClientMessage,Option),MessageError> { use Command::*; Ok(match self { CreateUser { password, } => (ClientMessage::UserCreate {username, password},None), // This should login and then get a token and save it. // For now because there are no tokens it saves the username // and password in cleartext :) Login { password, } => (ClientMessage::Auth{username,password: AuthMethod::Password(password)},None), SendMessage { body, room, } => (ClientMessage::Message{body, room_id: room.get_coord(), id: None},room.get_server()), Listen { room, } => (ClientMessage::SubscribeMessages{room_id: room.get_coord()},room.get_server()), GetMessages { room, count } => (ClientMessage::FetchMessages{room_id: room.get_coord(), count, end: fedichat::message::MessageId(i64::MAX as u64)},room.get_server()), Join { room, } => (ClientMessage::RoomJoin{room_id: room.get_coord()},room.get_server()), Leave { room, } => (ClientMessage::RoomLeave{room_id: room.get_coord()},room.get_server()), Create { room, } => (ClientMessage::RoomCreate{room_id: room.get_coord()},room.get_server()), Upload {file} => { let mut buf = Vec::with_capacity(4096); let mut file = File::open(file)?; file.read_to_end(&mut buf)?; (ClientMessage::MediaUpload{bytes: buf},None) }, Fetch {file: _, id} => (ClientMessage::MediaFetch{id: Uuid::parse_str(&id)?},None), GetState { room, path, } => (ClientMessage::StateRead{room_id: room.get_coord(), path: StatePath(path)},room.get_server()), WriteState { room, path, content, } => (ClientMessage::StateWrite{ room_id: room.get_coord(), path: StatePath(path), content: fedichat::state::StateValue::String(content) },room.get_server()), SendPost { body, user, } => (ClientMessage::MessagePost{body, user: user.clone()},Some(ServerAddr(user.server))), }) } pub fn into_signed_message(self, username: String) -> Result { let (message,target) = self.into_client_message(username)?; Ok(SignedClientMessage { message, target, timestamp: OffsetDateTime::now_utc(), // TODO: actually implement signatures signature: Box::new([0]) }) } pub fn needs_auth(&self) -> bool { match self { Command::Login{..} | Command::CreateUser{..} => false, _ => true } } // If a command needs multiple messages, like an auth message first // then call this pub fn generate_messages(self,username: String,token: Option, key: SigningKey) -> Result,MessageError> { let mut messages = Vec::with_capacity(2); let needs_auth = self.needs_auth(); if needs_auth && token.is_none() { return Err(MessageError::NoToken); } if needs_auth && let Some(token) = token { messages.push(SignedClientMessage { message: ClientMessage::Auth { username: username.clone(), password: AuthMethod::Token(token) }, target: None, timestamp: OffsetDateTime::now_utc(), signature: Box::new([0]) }) } messages.push(self.into_signed_message(username)?); // If this message is authenticating us then we should be generating // a new token and saving it for future commands if !needs_auth { messages.push(SignedClientMessage { message: ClientMessage::CreateToken, target: None, timestamp: OffsetDateTime::now_utc(), signature: Box::new([0])}); } // sign all the messages for message in messages.iter_mut() { message.sign(key.clone())?; } Ok(messages) } } #[derive(Error,Debug)] pub enum MessageError { #[error("Command needs a token. Login or create an account first.")] NoToken, #[error("Error while parsing uuid: {0}")] UuidError(#[from] uuid::Error), #[error("Error during file IO: {0}")] IoError(#[from] std::io::Error), #[error("Error while processing signature: {0}")] Signature(#[from] fedichat::client::SignatureError) } // example room: 1,-3,2@fedichat.net #[derive(Clone,Debug)] pub struct Room { pub coord: Vec, pub server: Option } impl Room { pub fn from_str(other: &str) -> Result { let pair: Vec<&str> = other.split('@').collect(); let (coords,server) = match pair.as_slice() { [coords,server] => (coords,Some(server.to_string())), [coords] => (coords,None), _ => return Err("Wrong number of @. Need exactly 1".to_string()) }; let coords: Vec<&str> = coords.split(',').collect(); let coord = coords.into_iter().map( |x| x.parse::().map_err(|_| format!("Failed to parse coordinate {}",x)) ).collect::,String>>()?; Ok(Room{coord,server}) } // I probably need to not use these type fields directly dont I pub fn get_coord(&self) -> fedichat::RoomId { fedichat::RoomId{coordinates: self.coord.clone()} } pub fn get_server(&self) -> Option { self.server.clone().map(|x| fedichat::ServerAddr(x)) } } pub fn user_from_str(s: &str) -> Result { match s.split('@').collect::>().as_slice() { [name,server] => Ok(fedichat::User { name: name.to_string(), server: server.to_string() }), _ => Err(format!("Username must contain exactly one @: {}",s)) } }