use anyhow::Result;
use owo_colors::OwoColorize;
use crate::api::client::{ApiClient, NewWebhook, WebhookChange};
use crate::api::models::{Webhook, WebhookDelivery};
use crate::exit;
use crate::output::Format;
use crate::render::sanitise;
use crate::render::table::{Cell, Table};
use crate::render::theme;
use crate::render::time;
pub async fn list(client: &ApiClient, project_reference: &str, format: Format) -> Result<()> {
let project = client.find_project(project_reference).await?;
let answer = client.webhooks(&project.id).await?;
if format.answered_in_json(&answer.raw) {
return Ok(());
}
let mut table = Table::new(&["aviso", "destino", "formato", "eventos", "último envío"]);
for webhook in &answer.body.webhooks {
table.push(vec![
Cell::styled(
sanitise::plain(&webhook.name),
if webhook.enabled {
theme::strong()
} else {
theme::muted()
},
),
Cell::styled(sanitise::plain(&webhook.url_hint), theme::muted()),
Cell::plain(format_word(&webhook.format)),
Cell::plain(events_word(webhook, answer.body.events.len())),
delivery_cell(webhook),
]);
}
table.print_or(format_args!(
"`{}` no tiene avisos. fundaia webhook add {} <url>",
project.name, project.slug
));
Ok(())
}
pub struct AddRequest<'a> {
pub project: &'a str,
pub url: &'a str,
pub name: Option<&'a str>,
pub format: Option<&'a str>,
pub events: &'a [String],
}
pub async fn add(client: &ApiClient, request: AddRequest<'_>, format: Format) -> Result<()> {
let project = client.find_project(request.project).await?;
let answer = client
.create_webhook(
&project.id,
&NewWebhook {
url: request.url,
name: request.name,
format: request.format,
events: request.events,
},
)
.await?;
if format.answered_in_json(&answer.raw) {
return Ok(());
}
let created = &answer.body;
println!();
println!(
" {} {} avisará a {} ({})",
theme::CHECK.style(theme::success()),
sanitise::plain(&project.name).style(theme::strong()),
sanitise::plain(&created.webhook.name).style(theme::strong()),
sanitise::plain(&created.webhook.url_hint).style(theme::muted()),
);
if created.webhook.format == "json" {
println!(
" {} {}",
"Secreto para comprobar la firma (X-Fundaia-Signature), no se volverá a mostrar:"
.style(theme::muted()),
created.signing_secret.style(theme::accent()),
);
}
println!(
" {}",
format!(
"fundaia webhook test {} {}",
project.slug,
shell_word(&created.webhook.name)
)
.style(theme::muted())
);
println!();
Ok(())
}
pub struct SetRequest<'a> {
pub project: &'a str,
pub webhook: &'a str,
pub name: Option<&'a str>,
pub events: Option<&'a [String]>,
pub enabled: Option<bool>,
}
pub async fn set(client: &ApiClient, request: SetRequest<'_>, format: Format) -> Result<()> {
if request.name.is_none() && request.events.is_none() && request.enabled.is_none() {
return Err(exit::Refused::invalid(
"nada que cambiar: usa --name, --events, --pause o --resume",
)
.into());
}
let project = client.find_project(request.project).await?;
let answer = client
.update_webhook(
&project.id,
request.webhook,
&WebhookChange {
name: request.name,
events: request.events,
enabled: request.enabled,
},
)
.await?;
if format.answered_in_json(&answer.raw) {
return Ok(());
}
let webhook = &answer.body.webhook;
println!(
" {} {} {}",
theme::CHECK.style(theme::success()),
sanitise::plain(&webhook.name).style(theme::strong()),
if webhook.enabled {
format!("avisa de: {}", webhook.events.join(", "))
} else {
"en pausa".to_owned()
}
.style(theme::muted()),
);
Ok(())
}
pub async fn remove(
client: &ApiClient,
project_reference: &str,
webhook: &str,
format: Format,
) -> Result<()> {
let project = client.find_project(project_reference).await?;
client.delete_webhook(&project.id, webhook).await?;
if format.is_json() {
crate::output::emit(&serde_json::json!({ "removed": webhook }));
return Ok(());
}
println!(
" {} {} ya no avisa a {}",
theme::CHECK.style(theme::success()),
sanitise::plain(&project.name).style(theme::strong()),
sanitise::plain(webhook).style(theme::strong()),
);
Ok(())
}
pub async fn test(
client: &ApiClient,
project_reference: &str,
webhook: &str,
format: Format,
) -> Result<()> {
let project = client.find_project(project_reference).await?;
let answer = client.test_webhook(&project.id, webhook).await?;
let delivery = &answer.body.delivery;
if !format.answered_in_json(&answer.raw) {
if delivery.delivered {
println!(
" {} {} {}",
theme::CHECK.style(theme::success()),
"Prueba entregada a".style(theme::muted()),
sanitise::plain(webhook).style(theme::strong()),
);
} else {
println!(
" {} {}",
theme::CROSS.style(theme::danger()),
failure_sentence(delivery),
);
}
}
if delivery.delivered {
Ok(())
} else {
Err(exit::Refused::upstream(failure_sentence(delivery)).into())
}
}
fn failure_sentence(delivery: &WebhookDelivery) -> String {
sanitise::plain(delivery.error.as_deref().unwrap_or("no llegó"))
}
fn format_word(format: &str) -> &str {
match format {
"slack" => "Slack",
"discord" => "Discord",
_ => "JSON firmado",
}
}
fn events_word(webhook: &Webhook, known: usize) -> String {
if !webhook.enabled {
return "en pausa".to_owned();
}
if known > 0 && webhook.events.len() == known {
return "todos".to_owned();
}
webhook.events.join(", ")
}
fn delivery_cell(webhook: &Webhook) -> Cell {
match &webhook.last_delivery {
None => Cell::styled("—", theme::muted()),
Some(delivery) => {
let when = time::relative_time(&delivery.at).unwrap_or_else(|| delivery.at.clone());
if delivery.delivered {
Cell::styled(format!("{} {when}", theme::CHECK), theme::success())
} else {
Cell::styled(format!("{} {when}", theme::CROSS), theme::danger())
}
}
}
}
fn shell_word(name: &str) -> String {
if name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
{
name.to_owned()
} else {
format!("'{}'", name.replace('\'', "'\\''"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_should_quote_a_name_with_a_space() {
assert_eq!(shell_word("Slack 2"), "'Slack 2'");
}
#[test]
fn it_should_leave_a_plain_name_bare() {
assert_eq!(shell_word("Discord"), "Discord");
}
}