use clap::{CommandFactory, Parser};
use std::str::FromStr;
#[tokio::main]
async fn main() {
let cli = SidekoCli::parse();
if let Err(e) = handle_cli(cli).await {
display_error(e);
std::process::exit(1);
}
}
async fn handle_cli(cli: SidekoCli) -> simple_slack_gen::SdkResult<()> {
init_logger(cli.verbose);
load_dotsideko();
let base_url_env_var = "SIMPLE_SLACK_GEN_BASE_URL";
match cli.command {
SidekoCommand::SidekoConfigSubcommand(
SidekoConfigSubcommand::Docs { output },
) => {
let markdown = clap_markdown::help_markdown::<SidekoCli>();
std::fs::write(&output, markdown)?;
log::info!("CLI documentation saved to {output}")
}
SidekoCommand::SidekoConfigSubcommand(
SidekoConfigSubcommand::Completions { shell },
) => {
let mut cmd = SidekoCli::command();
let cmd_name = cmd.get_name().to_string();
clap_complete::generate(shell, &mut cmd, cmd_name, &mut std::io::stdout());
}
SidekoCommand::SidekoConfigSubcommand(
SidekoConfigSubcommand::BaseUrl { unset, url },
) => {
if unset {
write_dotsideko(base_url_env_var, "", true);
} else if let Some(url) = url {
write_dotsideko(base_url_env_var, &url, false);
} else {
log::error!("No base url provided");
std::process::exit(1);
}
log::info!("Base URL updated")
}
SidekoCommand::ConversationsSubcommand(ConversationsSubcommand::List(req)) => {
let mut client = simple_slack_gen::Client::default();
if let Ok(base_url) = std::env::var(base_url_env_var) {
client = client.with_base_url(&base_url);
log::debug!("Using custom base url: {base_url}");
}
if let Ok(val) = std::env::var("SIMPLE_SLACK_GEN_AUTH") {
log::debug!("Adding oauth auth 'auth' (key=\"****\")");
client = client.with_auth(&val);
}
let res = client.conversations().list(req).await?;
println!(
"{}", serde_json::to_string_pretty(& res).unwrap_or_else(| _ |
serde_json::json!(& res) .to_string())
);
}
SidekoCommand::ChatSubcommand(ChatSubcommand::PostMessage(req)) => {
let mut client = simple_slack_gen::Client::default();
if let Ok(base_url) = std::env::var(base_url_env_var) {
client = client.with_base_url(&base_url);
log::debug!("Using custom base url: {base_url}");
}
if let Ok(val) = std::env::var("SIMPLE_SLACK_GEN_AUTH") {
log::debug!("Adding oauth auth 'auth' (key=\"****\")");
client = client.with_auth(&val);
}
let res = client.chat().post_message(req).await?;
println!(
"{}", serde_json::to_string_pretty(& res).unwrap_or_else(| _ |
serde_json::json!(& res) .to_string())
);
}
SidekoCommand::SidekoConfigSubcommand(
SidekoConfigSubcommand::SidekoAuthSubcommand(
SidekoAuthSubcommand::Auth { token },
),
) => {
write_dotsideko("SIMPLE_SLACK_GEN_AUTH", &token, false);
log::info!("Authentication added to CLI");
}
}
Ok(())
}
fn display_error(e: simple_slack_gen::Error) {
match &e {
simple_slack_gen::Error::Io(error) => log::debug!("IO Error: {:?}", error),
simple_slack_gen::Error::Request(error) => {
log::debug!("Request Error: {:?}", error)
}
simple_slack_gen::Error::Api(api_error)
| simple_slack_gen::Error::ContentType(api_error) => {
log::debug!("Response headers: {:?}", & api_error.headers);
if let Ok(val) = api_error.json::<serde_json::Value>() {
log::debug!(
"Body: {}", serde_json::to_string_pretty(& val).unwrap_or_else(| _ |
val.to_string())
);
} else if let Ok(text) = std::str::from_utf8(&api_error.content) {
log::debug!("Body: {text}",);
} else {
log::debug!("Unable to process body ({} bytes)", api_error.content.len())
}
}
simple_slack_gen::Error::DeserializeJson(error, _json_str) => {
log::debug!("JSON Error: {error}")
}
}
log::error!("{e}");
}
#[allow(unused)]
fn save_binary_response(
res: simple_slack_gen::BinaryResponse,
) -> Result<(), std::io::Error> {
log::debug!("Binary response headers: {:?}", & res.headers);
let content_type = res
.headers
.get("content-type")
.map(|val| val.to_str().unwrap_or_default());
let mut extension = "out".to_string();
if let Some(ct) = content_type {
if let Some(Some(suffix)) = mime_guess::get_mime_extensions_str(ct)
.map(|s| s.first())
{
extension = suffix.to_string()
} else {
log::warn!("Unsable to determine file extension from content type '{ct}'")
}
} else {
log::warn!(
"Unable to determine file extension from empty content type header in response"
)
}
let outpath = camino::Utf8PathBuf::from(format!("./output.{extension}"));
std::fs::write(&outpath, &res.content)?;
log::info!("Wrote {} bytes to {outpath}", res.content.len());
Ok(())
}
fn get_dotsideko_path() -> camino::Utf8PathBuf {
if let Ok(custom_dotsideko) = std::env::var("SIDEKO_CLI_CONFIG") {
camino::Utf8PathBuf::from_str(&custom_dotsideko)
.unwrap_or_else(|_| {
log::debug!("$SIDEKO_CLI_CONFIG set to: '{custom_dotsideko}'");
log::error!(
"$SIDEKO_CLI_CONFIG environment variable must be a valid path if set"
);
std::process::exit(1)
})
} else {
let home = std::env::var("HOME")
.unwrap_or_else(|_| {
log::error!(
"$HOME environment variable must be set for the CLI to function"
);
std::process::exit(1)
});
let buf = camino::Utf8PathBuf::from_str(&home)
.unwrap_or_else(|_| {
log::debug!("$HOME set to: '{home}'");
log::error!("$HOME environment variable must be a valid path");
std::process::exit(1)
});
buf.join(".sideko-cli")
}
}
fn load_dotsideko() {
let path = get_dotsideko_path();
if path.exists() {
log::debug!("Loading CLI config from {path}");
if let Err(e) = dotenv::from_path(path.clone()) {
log::debug!("Dotenv error: {:?}", e);
log::error!("Failed loading config from '{path}'");
std::process::exit(1);
}
log::debug!("Loaded config from {path}")
}
}
fn write_dotsideko(var: &str, val: &str, unset: bool) {
let sh_safe_val = shlex::try_quote(val)
.map(String::from)
.unwrap_or_else(|_| val.to_string());
let dotenv_entry = format!("{var}={sh_safe_val}");
let path = get_dotsideko_path();
let current_dotenv: Vec<String> = if path.exists() {
let dotenv_string = std::fs::read_to_string(path.clone())
.unwrap_or_else(|e| {
log::debug!("FS error: {:?}", e);
log::error!("Failed loading config from '{path}'");
std::process::exit(1);
});
dotenv_string.split("\n").map(String::from).collect()
} else {
vec![]
};
let mut new_dotenv: Vec<String> = vec![];
let mut replaced = false;
for line in current_dotenv {
if line.starts_with(&format!("{var}=")) {
if !unset {
new_dotenv.push(dotenv_entry.clone());
replaced = true;
}
} else {
new_dotenv.push(line);
}
}
if !unset && !replaced {
new_dotenv.push(dotenv_entry)
}
std::fs::write(&path, new_dotenv.join("\n"))
.unwrap_or_else(|e| {
log::debug!("FS error: {:?}", e);
log::error!("Failed updating config at '{path}'");
std::process::exit(1);
});
if unset {
log::debug!("{var} unset in {path}")
} else {
log::debug!("{var} updated in {path}")
}
}
fn init_logger(verbosity: u8) {
let self_module = std::env::var("CARGO_PKG_NAME").unwrap_or_default();
let mut builder = env_logger::builder();
if verbosity == 0 {
builder
.filter_module(&self_module, log::LevelFilter::Info)
.format_target(false)
.format_timestamp(None)
} else if verbosity == 1 {
builder.filter_module(&self_module, log::LevelFilter::Debug).format_target(false)
} else {
builder.filter_level(log::LevelFilter::Trace)
};
let _ = builder.try_init();
}
fn get_styles() -> clap::builder::Styles {
clap::builder::Styles::styled()
.usage(
anstyle::Style::new()
.bold()
.underline()
.fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Blue))),
)
.header(
anstyle::Style::new()
.bold()
.underline()
.fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Blue))),
)
.literal(
anstyle::Style::new()
.fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Green))),
)
.invalid(
anstyle::Style::new()
.bold()
.fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Red))),
)
.error(
anstyle::Style::new()
.bold()
.fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Red))),
)
.valid(
anstyle::Style::new()
.bold()
.underline()
.fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Green))),
)
.placeholder(
anstyle::Style::new()
.fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::White))),
)
}
#[derive(clap::Subcommand)]
enum SidekoConfigSubcommand {
#[command(name = "docs")]
Docs {
#[arg(long, default_value = "./CLI.md")]
output: camino::Utf8PathBuf,
},
#[command(name = "completions")]
Completions { #[arg(long)] shell: clap_complete::Shell },
#[command(name = "base-url")]
BaseUrl {
#[arg(long)]
url: Option<String>,
#[arg(long)]
unset: bool,
},
#[command(subcommand, name = "auth")]
SidekoAuthSubcommand(SidekoAuthSubcommand),
}
#[derive(clap::Subcommand)]
enum SidekoAuthSubcommand {
#[command(name = "auth")]
Auth { #[arg(long)] token: String },
}
#[derive(clap::Parser)]
#[command(version, propagate_version = true, name = "simple-slack-gen")]
struct SidekoCli {
#[command(subcommand)]
command: SidekoCommand,
#[arg(
long,
short = 'v',
action = clap::ArgAction::Count,
global = true,
help = "Increase logging verbosity"
)]
verbose: u8,
}
#[derive(clap::Parser)]
#[command(styles = get_styles())]
#[allow(clippy::enum_variant_names)]
enum SidekoCommand {
#[command(subcommand, name = "config")]
SidekoConfigSubcommand(SidekoConfigSubcommand),
#[command(subcommand, name = "conversations")]
ConversationsSubcommand(ConversationsSubcommand),
#[command(subcommand, name = "chat")]
ChatSubcommand(ChatSubcommand),
}
#[derive(clap::Subcommand)]
#[allow(clippy::enum_variant_names)]
enum ConversationsSubcommand {
#[command(name = "list")]
List(simple_slack_gen::resources::conversations::ListRequest),
}
#[derive(clap::Subcommand)]
#[allow(clippy::enum_variant_names)]
enum ChatSubcommand {
#[command(name = "post-message")]
PostMessage(simple_slack_gen::resources::chat::PostMessageRequest),
}
#[cfg(test)]
mod cli_tests {
use clap::Parser;
#[serial_test::serial]
#[tokio::test]
async fn test_cli_conversations_list_200_generated_success() {
let cli = super::SidekoCli::try_parse_from(
shlex::Shlex::new(
&["simple-slack-gen", "config", "auth", "auth"].join(" "),
),
)
.expect("failed parsing auth cli input");
super::handle_cli(cli).await.expect("failed running auth command");
let cli = super::SidekoCli::try_parse_from(
shlex::Shlex::new(
&[
"simple-slack-gen",
"conversations",
"list",
"--cursor",
"'dXNlcjpVMDYxTkZUVDI='",
"--exclude-archived",
"true",
"--limit",
"10",
"--team-id",
"T1234567890",
"--types",
"'public_channel,private_channel'",
]
.join(" "),
),
)
.expect("failed parsing cli input");
let result = super::handle_cli(cli).await;
println!("{:?}", & result);
assert!(result.is_ok())
}
#[serial_test::serial]
#[tokio::test]
async fn test_cli_chat_post_message_200_success_default() {
let cli = super::SidekoCli::try_parse_from(
shlex::Shlex::new(
&["simple-slack-gen", "config", "auth", "auth"].join(" "),
),
)
.expect("failed parsing auth cli input");
super::handle_cli(cli).await.expect("failed running auth command");
let cli = super::SidekoCli::try_parse_from(
shlex::Shlex::new(
&[
"simple-slack-gen",
"chat",
"post-message",
"--as-user",
"string",
"--attachments",
"string",
"--blocks",
"string",
"--channel",
"channel_id",
"--icon-emoji",
"string",
"--icon-url",
"string",
"--link-names",
"true",
"--mrkdwn",
"true",
"--parse",
"string",
"--reply-broadcast",
"true",
"--text",
"'Hello World!'",
"--thread-ts",
"string",
"--unfurl-links",
"true",
"--unfurl-media",
"true",
"--username",
"string",
]
.join(" "),
),
)
.expect("failed parsing cli input");
let result = super::handle_cli(cli).await;
println!("{:?}", & result);
assert!(result.is_ok())
}
}