1use crate::error::{AvError, Result};
2use crate::session::Session;
3
4pub enum NotifyTarget {
6 Slack(String),
7 Discord(String),
8 Generic(String),
9}
10
11impl NotifyTarget {
12 pub fn parse(url: &str) -> Result<Self> {
14 if let Some(webhook) = url.strip_prefix("slack://") {
15 Ok(Self::Slack(webhook.to_string()))
16 } else if let Some(webhook) = url.strip_prefix("discord://") {
17 Ok(Self::Discord(webhook.to_string()))
18 } else if url.starts_with("https://") || url.starts_with("http://") {
19 Ok(Self::Generic(url.to_string()))
20 } else {
21 Err(AvError::InvalidPolicy(format!(
22 "Invalid notify URL '{}'. Use slack://WEBHOOK_URL, discord://WEBHOOK_URL, or an HTTPS URL",
23 url
24 )))
25 }
26 }
27}
28
29pub async fn notify_start(target: &NotifyTarget, session: &Session) -> Result<()> {
31 let actions: Vec<String> = session.policy.actions.iter().map(|a| a.to_iam_action()).collect();
32 let text = format!(
33 "Audex session started\n\u{2022} Session: `{}`\n\u{2022} Role: `{}`\n\u{2022} Actions: `{}`\n\u{2022} Command: `{}`\n\u{2022} TTL: {}s",
34 &session.id[..8],
35 session.role_arn,
36 actions.join(", "),
37 session.command.join(" "),
38 session.ttl_seconds,
39 );
40 send(target, &text).await
41}
42
43pub async fn notify_end(target: &NotifyTarget, session: &Session, exit_code: Option<i32>) -> Result<()> {
45 let emoji = match session.status.to_string().as_str() {
46 "completed" => "\u{2705}",
47 "failed" => "\u{274c}",
48 _ => "\u{26a0}\u{fe0f}",
49 };
50 let text = format!(
51 "{} Audex session ended\n\u{2022} Session: `{}`\n\u{2022} Status: {}\n\u{2022} Command: `{}`{}",
52 emoji,
53 &session.id[..8],
54 session.status,
55 session.command.join(" "),
56 exit_code.map_or(String::new(), |c| format!("\n\u{2022} Exit code: {}", c)),
57 );
58 send(target, &text).await
59}
60
61async fn send(target: &NotifyTarget, text: &str) -> Result<()> {
62 let client = reqwest::Client::new();
63
64 let (url, body) = match target {
65 NotifyTarget::Slack(webhook) => {
66 let url = if webhook.starts_with("https://") {
67 webhook.clone()
68 } else {
69 format!("https://{}", webhook)
70 };
71 (url, serde_json::json!({ "text": text }))
72 }
73 NotifyTarget::Discord(webhook) => {
74 let url = if webhook.starts_with("https://") {
75 webhook.clone()
76 } else {
77 format!("https://{}", webhook)
78 };
79 (url, serde_json::json!({ "content": text }))
80 }
81 NotifyTarget::Generic(url) => {
82 (url.clone(), serde_json::json!({
83 "text": text,
84 "source": "audex",
85 }))
86 }
87 };
88
89 client
90 .post(&url)
91 .header("content-type", "application/json")
92 .json(&body)
93 .send()
94 .await
95 .map_err(|e| AvError::Sts(format!("Webhook notification failed: {}", e)))?;
96
97 Ok(())
98}