use clap::Parser;
use std::{io::Read, path::PathBuf, process::ExitCode, time::Duration};
use trotter::{
error::ResponseErr,
parse::{Gemtext, Symbol},
Actor, Status, Titan, UserAgent,
};
#[derive(thiserror::Error, Debug)]
enum TrotErr {
#[error("{0}")]
ActorErr(#[from] trotter::error::ActorError),
#[error("{0}")]
Response(#[from] trotter::error::ResponseErr),
#[error("Expected one of these: archiver, indexer, researcher, webproxy")]
BadUserAgent,
#[error("Upload failed: {0}")]
UploadIo(std::io::Error),
#[error("Couldn't guess the mimetype of the file you tried to upload. Try changing its file extension.")]
UploadMimeGuess,
}
#[derive(Parser)]
struct Cli {
url: String,
input: Option<Vec<String>>,
#[clap(short, long)]
cert: Option<PathBuf>,
#[clap(short, long)]
key: Option<PathBuf>,
#[clap(long)]
user_agent: Option<String>,
#[clap(long)]
timeout: Option<u64>,
#[clap(short, long)]
output: Option<String>,
#[clap(long, short)]
upload: Option<PathBuf>,
#[clap(short, long)]
token: Option<String>,
#[clap(short, long)]
gemtext_only: bool,
#[clap(short, long)]
no_color: bool,
#[clap(short, long)]
mime: bool,
#[clap(long)]
server_cert: bool,
#[clap(long)]
server_info: bool,
#[clap(long)]
server_fingerprint: bool,
}
async fn run() -> Result<(), TrotErr> {
let opt = Cli::parse();
let actor = Actor::default();
let actor = if let Some(cert) = opt.cert {
actor.cert_file(cert)
} else {
actor
};
let actor = if let Some(key) = opt.key {
actor.key_file(key)
} else {
actor
};
let actor = if let Some(agent) = opt.user_agent {
actor.user_agent(match agent.as_str() {
"archiver" => UserAgent::Archiver,
"indexer" => UserAgent::Indexer,
"researcher" => UserAgent::Researcher,
"webproxy" => UserAgent::Webproxy,
_ => return Err(TrotErr::BadUserAgent),
})
} else {
actor
};
let actor = if let Some(t) = opt.timeout {
actor.timeout(Duration::from_secs(t))
} else {
actor
};
let response = match opt.upload {
Some(path) => {
let mut file = std::fs::File::open(&path).map_err(|e| TrotErr::UploadIo(e))?;
let mut content: Vec<u8> = Vec::new();
file.read_to_end(&mut content)
.map_err(|e| TrotErr::UploadIo(e))?;
let t = Titan {
token: opt.token,
mimetype: mime_guess::from_path(&path)
.first()
.ok_or(TrotErr::UploadMimeGuess)?
.to_string(),
content,
};
actor.upload(&opt.url, t).await?
}
None => {
if let Some(input) = opt.input {
let mut input: String = input.iter().map(|x| format!("{x} ")).collect();
let _ = input.pop();
actor.input(&opt.url, &input).await?
} else {
actor.get(&opt.url).await?
}
}
};
if opt.mime {
if response.status == Status::Success.into() {
println!("{}", response.meta);
return Ok(());
}
return Err(TrotErr::Response(ResponseErr::UnexpectedStatus {
expected: Status::Success,
received: response.status.into(),
meta: response.meta,
}));
}
if opt.server_fingerprint {
println!("{}", response.certificate_fingerprint()?);
return Ok(());
}
if opt.server_cert {
println!("{}", response.certificate_pem()?);
return Ok(());
}
if opt.server_info {
println!("{}", response.certificate_info()?);
return Ok(());
}
if let Some(output) = opt.output {
response.save_to_path(output)?;
return Ok(());
}
let text = if opt.gemtext_only {
response.gemtext()?
} else {
response.text()?
};
if !opt.no_color && response.is_gemtext() {
for g in Gemtext::parse(&text).inner() {
match g {
Symbol::Text(a) => print!("{a}"),
Symbol::Link(a, b) => print!("\x1b[0;4m{b}\x1b[0m \x1b[2m{a}"),
Symbol::List(a) => print!("β’ {a}"),
Symbol::Quote(a) => print!("\x1b[33;3;1mΒ« {a} Β»"),
Symbol::Header1(a) => print!("\x1b[32;1mβ {a}"),
Symbol::Header2(a) => print!("\x1b[36;1mβ {a}"),
Symbol::Header3(a) => print!("\x1b[34;1mβ {a}"),
Symbol::Codeblock(a, b) => {
if !a.is_empty() {
print!("\x1b[35;2m{a}\x1b[0m\n")
}
print!("\x1b[35m{b}")
}
}
println!("\x1b[0m");
}
} else {
println!("{text}");
}
Ok(())
}
#[tokio::main]
async fn main() -> ExitCode {
match run().await {
Err(e) => match e {
TrotErr::Response(ResponseErr::UnexpectedStatus {
expected: _,
received,
meta,
}) => {
println!("{meta}");
ExitCode::from(received.value())
}
_ => {
eprintln!("π Trot error :: {e}");
ExitCode::from(1)
}
},
Ok(_) => ExitCode::from(0),
}
}