289 lines
9.1 KiB
Rust
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))
|
|
}
|
|
}
|