Files

289 lines
9.1 KiB
Rust

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<String>,
#[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<String>
},
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<String>,
#[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<ServerAddr>),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<SignedClientMessage,MessageError> {
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<String>, key: SigningKey)
-> Result<Vec<SignedClientMessage>,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<i64>,
pub server: Option<String>
}
impl Room {
pub fn from_str(other: &str) -> Result<Self,String> {
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::<i64>().map_err(|_| format!("Failed to parse coordinate {}",x))
).collect::<Result<Vec<i64>,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<fedichat::ServerAddr> {
self.server.clone().map(|x| fedichat::ServerAddr(x))
}
}
pub fn user_from_str(s: &str) -> Result<fedichat::User,String> {
match s.split('@').collect::<Vec<&str>>().as_slice() {
[name,server] =>
Ok(fedichat::User {
name: name.to_string(),
server: server.to_string()
}),
_ => Err(format!("Username must contain exactly one @: {}",s))
}
}